SSH public-key authentication
SSH via PIV means the private key never leaves the card: ssh-agent loads
it through PKCS#11 (OpenSC), signs the challenge on-card after a PIN prompt,
and the server authorizes it like any other public key in
authorized_keys. No card-specific server-side support needed.
Note
Everything below that references cryptnox-default applies to a card
provisioned with that specific built-in profile — one option among
several (see Pre-personalization profiles), not necessarily what any given
card actually has. Check which profile your card was provisioned with
(factory piv preperso export-config, or ask whoever personalized it)
before assuming this section applies to you.
Warning
9C cannot be used for SSH on the ``cryptnox-default`` profile.
Steps 3/4 below fail with Permission denied (publickey) /
agent refused operation even with the correct PIN, with or without
ssh-agent in the loop — see Why it fails on cryptnox-default. Use
9A with the SIGN role added instead — the built-in ssh profile
does exactly this — see Fixing it: give 9A the SIGN role below.
How it has to look
The applet only performs challenge-response signing (what SSH auth needs)
for keys created with a SIGN-capable role. On cryptnox-default that’s
slot 9C — but 9C’s PIN policy makes it unusable for SSH regardless (see
below). 9A (PIV Authentication) is AUTHENTICATE-role only on
cryptnox-default and returns 6985 on a sign attempt, so it needs a
profile change before it can be used — the built-in ssh profile changes
only 9A’s role (no extra objects, unlike ms-logon, which also adds
SIGN+AUTHENTICATE to 9A but includes an importable RSA-2048 object for
AD-issued credentials you don’t need here) — see Pre-personalization profiles
and Fixing it: give 9A the SIGN role.
This guide’s steps below use whichever slot has your key + certificate — Quick start: a working PIV card puts them on 9C by default, which per the warning above will not work for SSH. Provision 9A instead (next section) before following Steps 0–4.
Why it fails on cryptnox-default
pkcs11-tool --login --signagainst the 9C key succeeds, but prompts for the PIN twice — once for normal login, then again for “context specific PIN”. That second prompt is PKCS#11’sCKA_ALWAYS_AUTHENTICATE: a fresh re-login is required immediately before every single signing operation, not just once per session.ssh-add -sthenssh -I(agent-mediated) fails: the agent only ever prompts once, at load time, and has no way to supply that second re-authentication later when a connection actually needs a signature — henceagent refused operation.Bypassing the agent entirely —
ssh -I "$PKCS11_MODULE"run directly, foreground, no agent involved — fails the same way. OpenSSH’s own built-in PKCS#11 client only performs the normal login, not the context-specific re-loginCKA_ALWAYS_AUTHENTICATEdemands before signing. Only tools that explicitly implement that second step (likepkcs11-tool) can use this key at all.
Why it has to be 9A specifically: pkcs15-tool --dump shows why 9C
behaves this way — its private key object reports
Usage: [0x204], sign, nonRepudiation. The nonRepudiation bit is what
triggers OpenSC’s always-reauth handling; it matches PIV convention (slot
9C, “Digital Signature”, is spec’d for non-repudiation, so the standard
expects fresh PIN verification before each signature). This is assigned by
OpenSC’s PIV driver for key reference 0x9C specifically, independent of the
card’s own profile or certificate — 9A’s private key object reports plain
Usage: [0x04], sign, with no nonRepudiation bit, so it is not
subject to the always-reauth requirement. 9A is also the slot PIV itself
designates for cardholder-to-system authentication, and its access mode on
cryptnox-default requires a PIN — over the contact interface only;
9A’s contactless access mode is NEVER, so none of this works over a
contactless-only reader (see Prerequisites below).
Fixing it: give 9A the SIGN role
You don’t need the full ms-logon profile for this — the built-in ssh
profile changes only 9A’s role (AUTHENTICATE -> AUTHENTICATE,
SIGN); everything else is identical to cryptnox-default. See
Pre-personalization profiles for the full built-in profile list.
This only applies at pre-personalization — it defines the key’s role at
the structural level, not something a later piv perso step can change.
If your card already has a different profile applied (e.g. it already went
through Quick start: a working PIV card on 9C), it needs a factory-level applet
reinstall before this profile can be loaded — that’s a manufacturing
operation outside this guide’s (and this CLI’s) scope; a blank/pre-perso
card needs no such step.
$ cryptnox-id factory piv preperso load-config --profile ssh --default-keys
$ cryptnox-id piv perso set-puk --default-keys
$ cryptnox-id piv perso set-pin --default-keys
$ cryptnox-id piv perso generate-key --slot 9A --algorithm ECCP256 \
--out 9a.pub.pem --default-keys
$ cryptnox-id piv perso self-sign-cert --slot 9A --subject "CN=Test User" \
--public-key 9a.pub.pem --out 9a.crt.pem
$ cryptnox-id piv perso import-cert --slot 9A --cert 9a.crt.pem
$ cryptnox-id piv perso write-standard-objects --default-keys
Warning
Every step above must explicitly say --slot 9A. Quick start: a working PIV card
defaults everywhere to 9C, and copy-pasting those defaults here silently
provisions the wrong slot.
Note
piv perso smoke-test defaults to --slot 9C. On this profile 9C
was never populated, so the plain command fails with a misleading
Authentication method blocked / “PIN or PUK is likely blocked” error —
the PIN is not actually blocked; there’s simply no key on 9C. Always run
piv perso smoke-test --slot 9A here.
With that done, the card has a SIGN-capable, PIN-protected key on 9A and Steps 0–4 below work as written.
Prerequisites
A card already through Quick start: a working PIV card (PIN set, a key + certificate in the slot you’re using).
OpenSC (
opensc-pkcs11.so) installed on the client. Debian/Ubuntu:apt install opensc; Fedora:dnf install opensc; macOS:brew install opensc.ssh-agentrunning (eval "$(ssh-agent)").A contact reader — 9A’s contactless access mode is
NEVERon bothcryptnox-defaultandssh, so every step below fails over a contactless-only reader, distinctly from the 9C issue above. The card behaves identically whether inserted or read by a different physical reader, as long as it’s the contact interface — reader model doesn’t matter, only contact vs. contactless.
Step 0 — find the PKCS#11 module path
$ find / -name "opensc-pkcs11.so" 2>/dev/null
Typical locations: /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so (Debian/Ubuntu),
/usr/lib64/pkcs11/opensc-pkcs11.so (Fedora), /usr/local/lib/opensc-pkcs11.so
(Homebrew). The rest of this guide calls it $PKCS11_MODULE.
Step 1 — get the SSH public key off the card
$ ssh-keygen -D "$PKCS11_MODULE" -e
This lists every card object OpenSC can present as an SSH key — one line per
certificate/key pair. If the card has more than one usable slot populated
(e.g. both 9C and a retired key-management slot), match by fingerprint/label
against pkcs15-tool --list-certificates.
Step 3 — load it into ssh-agent
$ ssh-add -s "$PKCS11_MODULE"
Prompts for the PIV PIN once (per ssh-agent lifetime, not per connection —
the applet’s PIN state persists until the card is removed or the session
ends). ssh-add -l confirms the card key is loaded.
Note
On cryptnox-default this step succeeds (Card added) and the key
lists in ssh-add -l — but see the warning at the top of this guide:
the actual sign at connection time still fails for 9C. A successful
ssh-add -s does not mean the setup will work end-to-end.
Step 4 — connect
$ ssh -I "$PKCS11_MODULE" user@host
Or make it permanent in ~/.ssh/config so plain ssh user@host picks it
up automatically:
Host host
PKCS11Provider /usr/lib64/pkcs11/opensc-pkcs11.so
Troubleshooting
No keys listed in Step 1 — the slot has a key but no certificate (or vice versa); OpenSC needs both to expose an SSH-usable object. Check
pkcs15-tool --list-certificatesandpkcs15-tool --list-keys.Sign operation fails / card returns 6985 — the slot’s role doesn’t include
SIGN. On unmodifiedcryptnox-defaultthat’s 9A — see Fixing it: give 9A the SIGN role.``ssh-add -s`` succeeds, PIN is correct, but connecting still fails with “Permission denied (publickey)” or “agent refused operation”, and you’re using 9C — this is the
cryptnox-default/9C issue described in Why it fails on cryptnox-default above, not a misconfiguration. It reproduces identically with or withoutssh-agentin the loop, so retrying agent setup won’t fix it — switch to 9A instead (Fixing it: give 9A the SIGN role).``piv perso smoke-test`` says the PIN/PUK is blocked, but ``piv pin status`` shows full tries remaining — you’re on 9A and forgot
--slot 9A(the command defaults to 9C); see the note in Fixing it: give 9A the SIGN role.Login fails against a PKCS#11 tool (``CKR_USER_NOT_LOGGED_IN`` / similar) even with the correct PIN, and the reader is contactless — 9A’s contactless access mode is
NEVER; this is the card correctly refusing the interface, not a PIN or software problem. Switch to a contact reader — see Prerequisites.
Next steps
Quick start: a working PIV card — provision the key and certificate this guide builds on
Pre-personalization profiles — profile YAML grammar (role, access mode, mechanisms)