Skip to content

fix(keystore): YubiKey backend fails closed for any PIN- or touch-protected key (no PinProvider wired in) #777

Description

@christopherwxyz

Context

mkit-keystore's YubiKey backend (backend_yubikey.rs) fails closed for any key that actually requires interactive PIN or touch confirmation — which is the standard, recommended configuration for both the OpenPGP and PIV applets. In practice this means the backend only works for a YubiKey deliberately configured with no PIN/touch requirement at all, not for a typical real-world setup.

Found while investigating whether makechain's CLI (mkch, which depends on mkit-keystore = "=0.3.0" and exposes --backend yubikey) could offer genuine hardware-backed Ed25519 signing as a stronger alternative to its OS-keychain-only backends (see officialunofficial/makechain#1229 / #1231 for that discussion).

The gap

ensure_openpgp_interaction_available (rust/crates/mkit-keystore/src/backend_yubikey.rs) unconditionally returns an error for any OpenPGP-applet signing attempt:

fn ensure_openpgp_interaction_available(_label: &str) -> Result<()> {
    Err(Error::AuthenticationRequired(
        "YubiKey OpenPGP signing requires an explicit PIN prompt provider; PINs via environment variables are not supported"
            .into(),
    ))
}

ensure_piv_interaction_available is conditional but still blocks the common case — any key with pin_policy != PinPolicy::Never, or touch_policy of Always/Cached:

fn ensure_piv_interaction_available(key: &PivSigningKey) -> Result<()> {
    if key.pin_policy != PinPolicy::Never {
        return Err(Error::AuthenticationRequired(
            "YubiKey PIV signing requires an explicit PIN prompt provider; PINs via environment variables are not supported"
                .into(),
        ));
    }
    if matches!(key.touch_policy, TouchPolicy::Always | TouchPolicy::Cached) {
        return Err(Error::AuthenticationRequired(
            "YubiKey PIV signing key requires touch, but no bounded touch prompt flow is available"
                .into(),
        ));
    }
    Ok(())
}

Since a PIN or touch requirement is the YubiKey-recommended (and often default) posture for both applets, this means the backend is effectively non-functional for most real hardware today — not a hardening choice, just an unfinished feature. The error messages themselves ("requires an explicit PIN prompt provider") describe the missing piece precisely.

Prior art already in this repo

mkit-attest's ExternalSigner just implemented exactly this missing piece for a different signing pathway (#745, closing #694): a PinProvider trait + default TtyPinProvider (interactive terminal prompt, stderr prompt / stdin read, best-effort no-echo via stty), explicitly never sourcing a PIN from argv or an environment variable — matching the security rationale already documented in mkit-keystore's own error strings above.

That fix lives in the ExternalSigner/subprocess-signer path (used by reference binaries like mkit-sign-ctap, which itself signs via WebAuthn/P-256, not Ed25519) — it doesn't touch mkit-keystore's in-process YubiKey backend at all. The same PinProvider pattern (or something API-compatible with it) is the natural fix here too.

Proposed scope

  • Add a PinProvider-style hook to mkit-keystore's YubiKey backend (mirroring mkit-attest's trait shape where reasonable, so callers embedding both crates get one consistent interactive-PIN UX rather than two).
  • Wire it into both ensure_openpgp_interaction_available and ensure_piv_interaction_available so PIN-required OpenPGP and PIV keys can actually sign.
  • Bounded touch-prompt flow for PIV Always/Cached touch policies (the second error above), analogous to ExternalSigner's bounded PIN round-trip (8 attempts) — some bounded/timeout UX rather than blocking forever on physical touch.
  • Test coverage: today there's no test exercising a PIN-required key actually completing a sign (only tests asserting the current fail-closed behavior) — add real-subprocess or mocked-device tests for the round trip, matching the pattern mkit-attest's PR feat(attest): implement external-signer PinPrompt/PinResponse round-trip; deprecate --pin on argv #745 used.
  • Document the resulting UX (what a user sees when their PIN-protected YubiKey is used) in mkit-keystore's README.

Non-goals

Downstream impact

makechain's CLI (mkch) exposes --backend yubikey (via mkit-keystore) but has no README documentation, no test coverage of the YubiKey path, and — per this issue — the feature doesn't currently work for a real-world PIN/touch-configured device. Fixing this here unblocks offering genuine hardware-backed signing as a documented option in mkch, rather than a silently-broken one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    attestAttestation, signing, verifier, or signer behaviorbugSomething isn't workingsecuritySecurity-sensitive bug or hardening work

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions