diff --git a/kms/yubikey/yubikey.go b/kms/yubikey/yubikey.go index a39e5f05..3c51dc28 100644 --- a/kms/yubikey/yubikey.go +++ b/kms/yubikey/yubikey.go @@ -717,7 +717,8 @@ type syncSigner struct { func (s *syncSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { m.Lock() defer m.Unlock() - return s.Signer.Sign(rand, digest, opts) + sig, err := s.Signer.Sign(rand, digest, opts) + return sig, wrapTouchHint(err) } // syncDecrypter wraps a crypto.Decrypter with a mutex to avoid the error "smart @@ -730,7 +731,27 @@ type syncDecrypter struct { func (s *syncDecrypter) Decrypt(rand io.Reader, msg []byte, opts crypto.DecrypterOpts) ([]byte, error) { m.Lock() defer m.Unlock() - return s.Decrypter.Decrypt(rand, msg, opts) + plaintext, err := s.Decrypter.Decrypt(rand, msg, opts) + return plaintext, wrapTouchHint(err) +} + +// wrapTouchHint annotates a 0x6982 "security status not satisfied" card error +// with a hint that the slot's touch policy may be waiting for a physical touch. +// The status word is not touch-specific -- it can also mean an unsatisfied PIN +// or, without the mutex above, concurrent access -- so the hint is conditional. +// +// The status is detected through an anonymous interface because piv-go's error +// type is unexported. If a future piv-go release exposes a sentinel for this +// status, this can become a plain errors.Is check. +func wrapTouchHint(err error) error { + if err == nil { + return nil + } + var sc interface{ Status() uint16 } + if errors.As(err, &sc) && sc.Status() == 0x6982 { + return fmt.Errorf("%w; if the key's slot has a touch policy, touch the blinking YubiKey and retry", err) + } + return err } var _ apiv1.CertificateManager = (*YubiKey)(nil) diff --git a/kms/yubikey/yubikey_test.go b/kms/yubikey/yubikey_test.go index a8268c9a..556b6dfc 100644 --- a/kms/yubikey/yubikey_test.go +++ b/kms/yubikey/yubikey_test.go @@ -1459,3 +1459,30 @@ Vpmq2Sh/xT5HiFuhf4wJb0bK } } + +func TestWrapTouchHint(t *testing.T) { + // A 0x6982 "security status not satisfied" error gets the touch hint, and the + // original error stays unwrappable. + base := cardStatusErr(0x6982) + got := wrapTouchHint(base) + assert.Contains(t, got.Error(), "touch") + assert.ErrorIs(t, got, base) + + // A different status word is passed through unchanged. + other := cardStatusErr(0x6a80) + assert.Equal(t, other.Error(), wrapTouchHint(other).Error()) + assert.NotContains(t, wrapTouchHint(other).Error(), "touch") + + // A nil error stays nil; a plain error without Status() passes through. + assert.NoError(t, wrapTouchHint(nil)) + plain := errors.New("boom") + assert.ErrorIs(t, wrapTouchHint(plain), plain) + assert.NotContains(t, wrapTouchHint(plain).Error(), "touch") +} + +// cardStatusErr is a minimal card-style error that exposes Status() like piv-go's +// unexported apduErr, so wrapTouchHint can be tested without hardware. +type cardStatusErr uint16 + +func (e cardStatusErr) Error() string { return fmt.Sprintf("smart card error %04x", uint16(e)) } +func (e cardStatusErr) Status() uint16 { return uint16(e) }