Skip to content

Say when a failed PIN verification tried the default PIN#1075

Open
TicketWindowSeat wants to merge 1 commit into
smallstep:masterfrom
TicketWindowSeat:kevin/yubikey-default-pin-error
Open

Say when a failed PIN verification tried the default PIN#1075
TicketWindowSeat wants to merge 1 commit into
smallstep:masterfrom
TicketWindowSeat:kevin/yubikey-default-pin-error

Conversation

@TicketWindowSeat

Copy link
Copy Markdown

Name of feature:

Bug fix: when a PIN verification fails and the PIN in use was the library's fallback — the YubiKey factory default — the error now says so, and names the URI options that provide the right one.

Pain or issue this feature alleviates:

When no PIN is provided, New falls back to the YubiKey factory default PIN 123456 (documented, intentional). The trouble is what happens when the card's PIN is not the default: CreateSigner and CreateDecrypter pass the guessed PIN with piv.PINPolicyAlways, so every Sign or Decrypt sends a VERIFY with a wrong PIN. Each attempt consumes one of the card's PIN retries (3 by default), and the error the user sees is:

verify pin: smart card error 63c2: verification failed (2 retries remaining)

Nothing tells the user the library chose that PIN itself — the error reads like a typo in a PIN they never typed. Retrying the same innocent command twice more blocks the PIN, and a blocked PIN means a PUK ceremony or, as in smallstep/cli#1492, a PIV reset that destroys the PIV keys. In #1492 the user's pin-value was correct but didn't reach the step-kms-plugin subprocess; the silent fallback did the rest, and the maintainer confirmed the only way out was a reset. step-kms-plugin#83 shows the same opaque error confusing another user.

With this change the same failure reads:

verify pin: smart card error 63c2: verification failed (2 retries remaining); the YubiKey factory default PIN was tried because no PIN was available; provide one with pin-value, pin-source or pin-prompt

which is self-diagnosing before the last retry is gone. The clause says "no PIN was available" rather than "was provided" deliberately: uri.Pin() also resolves empty when a pin-source file is unreadable or a pin-prompt has no terminal, and the message stays true in those cases too.

Why is this important to the project (if not answered above):

This library drives an HSM-like device where the failure budget by default is three attempts, and typical CLI usage (stepstep-kms-plugin) spends one process invocation — and therefore one silent VERIFY — per operation. A misconfiguration that costs nothing anywhere else in the KMS surface here consumes an unrecoverable hardware resource. The default-PIN fallback itself is reasonable and stays exactly as documented; the fix is only that failing with a PIN the user never chose now says so.

New records whether the PIN came from the options or from the fallback, syncSigner/syncDecrypter carry that flag, and a small helper appends the context when the error matches piv.AuthErr (which also covers 6983: authentication method blocked — the "last retry already gone" case). Errors from an explicitly provided PIN — including an explicit pin-value=123456 — are untouched, as is every success path and every non-PIN error.

Is there documentation on how to use this feature? If so, where?

No usage change. New's doc comment now warns that a failed PIN verification consumes one of the card's PIN retries and points at pin-value, pin-source and pin-prompt (the prompt option added in #810). The error message itself is the documentation that matters: it names the three URI options at the moment the user needs them.

In what environments or workflows is this feature supported?

All platforms the yubikey KMS builds on (build tag cgo && !noyubikey). The new tests are hardware-free (stubbed pivCards/pivOpen, a failing signer/decrypter injected into the stub) and run in CI: they pin the wrapped message, that errors.Is/errors.As still reach the original error and piv.AuthErr through the wrap (including the blocked-PIN AuthErr{0} case), that no hint appears when a PIN was provided, and that an unreadable pin-source falls back to the default with defaultPIN provenance recorded. Manually verified on a spare YubiKey 4 (fw 4.3.3, pcsc-lite, 32-bit Linux) with a non-default PIN: the unpatched library burned a real retry with the opaque 63c2 ... (2 retries remaining) error; the patched library burned one with the self-diagnosing error (correctly rendering piv-go's singular "1 retry remaining"); a sign with the correct pin-value then succeeded and the card reset its retry counter to 3. Full package suite also run on Windows amd64 (CGO_ENABLED=1) and the same 32-bit Linux box. What CI cannot catch: it runs Linux-only with no PC/SC stack or card hardware, so the real VERIFY exchange, the retry-counter decrement, and the end-to-end rendering of this error rest on the manual verification above — the stubbed tests cover everything else.

In what environments or workflows is this feature explicitly NOT supported (if any)?

No behavior change: same PINs sent, same operations, same error values (errors.Is/errors.As compatible) — only the message gains a suffix, and only when the fallback PIN was the one that failed. Code that string-matches the exact old error text would see the suffix; error-value comparisons are unaffected.

Supporting links/other PRs/issues:

Reviewed and refined with AI assistance; all changes reviewed, run, and verified by me.

💔Thank you!

When no PIN is available from pin-value, pin-source or pin-prompt,
New falls back to the YubiKey factory default PIN. If the card uses a
different PIN, every Sign or Decrypt then sends that wrong PIN: each
attempt consumes one of the card's PIN retries and the error

    verify pin: smart card error 63c2: verification failed (2 retries remaining)

reads as if the user mistyped a PIN, when it was this package that
chose it. Three such runs block the PIN. This happened in
smallstep/cli#1492, where a pin that did not reach the step-kms-plugin
subprocess ended in a locked YubiKey whose PIV application had to be
reset.

Record in New whether the PIN came from the options or from the
default, and when a PIN verification fails with the default PIN, say
so and name the remedies:

    verify pin: smart card error 63c2: verification failed (2 retries
    remaining); the YubiKey factory default PIN was tried because no
    PIN was available; provide one with pin-value, pin-source or
    pin-prompt

The default itself stays documented, intentional behavior; only the
error changes.

Fixes smallstep#1074
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: kms/yubikey silently tries the factory default PIN and burns PIV PIN retries

2 participants