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
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.
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 onmkit-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:ensure_piv_interaction_availableis conditional but still blocks the common case — any key withpin_policy != PinPolicy::Never, ortouch_policyofAlways/Cached: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'sExternalSignerjust implemented exactly this missing piece for a different signing pathway (#745, closing #694): aPinProvidertrait + defaultTtyPinProvider(interactive terminal prompt, stderr prompt / stdin read, best-effort no-echo viastty), explicitly never sourcing a PIN from argv or an environment variable — matching the security rationale already documented inmkit-keystore's own error strings above.That fix lives in the
ExternalSigner/subprocess-signer path (used by reference binaries likemkit-sign-ctap, which itself signs via WebAuthn/P-256, not Ed25519) — it doesn't touchmkit-keystore's in-process YubiKey backend at all. The samePinProviderpattern (or something API-compatible with it) is the natural fix here too.Proposed scope
PinProvider-style hook tomkit-keystore's YubiKey backend (mirroringmkit-attest's trait shape where reasonable, so callers embedding both crates get one consistent interactive-PIN UX rather than two).ensure_openpgp_interaction_availableandensure_piv_interaction_availableso PIN-required OpenPGP and PIV keys can actually sign.Always/Cachedtouch policies (the second error above), analogous toExternalSigner's bounded PIN round-trip (8 attempts) — some bounded/timeout UX rather than blocking forever on physical touch.mkit-attest's PR feat(attest): implement external-signer PinPrompt/PinResponse round-trip; deprecate --pin on argv #745 used.mkit-keystore's README.Non-goals
ExternalSigner/CTAP2 pathway (mkit-sign-ctap) — that's already fixed by feat(attest): implement external-signer PinPrompt/PinResponse round-trip; deprecate --pin on argv #745 and is architecturally separate (subprocess-delegated, WebAuthn/P-256).Downstream impact
makechain's CLI (mkch) exposes--backend yubikey(viamkit-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 inmkch, rather than a silently-broken one.