Skip to content

Resolve the mgmt-key algorithm by probing - #124

Merged
framefilter merged 3 commits into
framefilter:mainfrom
episource:feature/piv-mgmt-key-algorithm-probe
Sep 8, 2026
Merged

framefilter merged 3 commits into
framefilter:mainfrom
episource:feature/piv-mgmt-key-algorithm-probe

Conversation

@episource

Copy link
Copy Markdown
Contributor

Currently keyroost uses the management key algorithm reported by GET METADATA. This is a yubico extension APDU. For cards not supporting this extension, 3-DES was used unconditionally. This PR adds probing if GET METADATA is not implemented by the card/token.

Cards that don't answer GET METADATA (pre-5.3 YubiKey firmware, non-Yubico
PIV applets) left us assuming Triple-DES for the 9B management key. That is
wrong for any such card provisioned with an AES management key: GENERAL
AUTHENTICATE is then issued under the wrong P1 and the card rejects it.

Resolve the algorithm instead of guessing:

- GET METADATA still wins outright when the card answers it.
- Otherwise probe every algorithm's GENERAL AUTHENTICATE P1 with a bare
  witness request (step 1 only -- no key-derived material reaches the card)
  and collect the ones the card starts with SW 9000. Narrow that set to the
  algorithms whose key matches the length in hand; if more than one remains
  (only 3DES and AES-192 collide, both 24 bytes) prefer 3DES, the historical
  default. If the card accepted no probe at all, fall back to the key length
  alone -- no worse than before.

A real transport failure during probing propagates as itself; only a
non-9000 status word means "not this P1, try the next".

New: PivSession::reported_management_key_algorithm (metadata answer with no
fallback) and resolve_management_key_algorithm. The 9 GUI PIV admin flows and
the CLI's open_piv_authed now call the resolver; the CLI keeps its tailored
wrong-length message for the metadata-present case. PivBadKeyLength's message
is broadened to cover "matches no algorithm this card accepts".

The length->candidates table and the probe-result selection are pure
functions (mgmt_algs_for_key_len, pick_mgmt_alg) with unit tests; the probe
loop itself needs a live card, consistent with the rest of this module.
@episource

Copy link
Copy Markdown
Contributor Author

Cryptnox OpenFIPS201 variant uses AES management as default, but does not implement yubico extension command GET METADATA. Setting management key for this type of card enabled by cryptnox/cryptnox-id-cli#12 .

framefilter and others added 2 commits September 7, 2026 13:45
…the CLI probe path

The CLI wrapper around the management-key algorithm probe mapped every
error from the resolver to the "does not match any algorithm this card
accepts" message. The transport layer takes care to propagate a
mid-probe transport failure (card pulled, reader gone) as itself; the
wrapper undid that. Only PivBadKeyLength now gets the friendly wording.
The comment above it also said the probe ran only for 24-byte keys,
which is not what the resolver does. Maintainer-side fixup for framefilter#124.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpyGDFxYw5DSD97HXBAc2q
@framefilter

framefilter commented Sep 8, 2026 •

Copy link
Copy Markdown
Owner

Thanks @episource, this closes a known gap.

Two questions:

  1. Which hardware did you verify on? If it was the Cryptnox OpenFIPS201: did the card reject the wrong P1s at step 1, or accept all four and fall through to the key-length table?
  2. Any objection to dropping PivSession::management_key_algorithm in a follow-up? It has no callers left.

I pushed two commits on top of yours: 1dd8bbb narrows the CLI's map_err to PivBadKeyLength so a transport failure mid-probe isn't reported as a wrong-length key, and corrects the comment above it; 8003c34 adds the changelog fragment. Let me know if there's anything that doesn't meet your intent, happy to revisit.

@framefilter
framefilter merged commit 7e67f32 into framefilter:main Sep 8, 2026
8 checks passed
@episource

episource commented Sep 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks for catching cli's error handling being to broad.

I've indeed tested management auth with a Cryptnox PIV card. The vendor claims this card uses plain OpenFIPS201 v2 as certified per Nist CMVP #5280. I bought this card for OpenFIPS201 v2 testing.

I've only done management key GENERAL AUTHENTICATE tests using this card and was able to use Keyroost to successfully authenticate and create a key on that device. Didn't do any further tests with the Cryptnox card, therefore I wouldn't claim these cards fully supported (though more functionality might work, just not tested).

Also consider, the default cryptnox-id (pre) perso sequence does not initialize the slot 9B management key object. The cryptnox-id's default way is SCP03 admin commands only, not the PIV standard PUT DATA and GENERATE ASYMMETRIC KEY commands protected by 9B-management-key backed GENERAL AUTHENTICATE. Keyroost however relies on the PUT DATA/GENERATE ASYMMETRIC KEY commands. OpenFIPS201 v2 has full support for this, but the currently released cryptnox tooling does not even support initializing the 9B key. I've created cryptnox/cryptnox-id-cli#12 to add this. Without 9B-management-key set, the PIV standard PUT DATA/GENERATE ASYMMETRIC KEY commands are blocked!

For my tests I've used below cryptnox profile. It's derived from cryptnox-default profile, but disables contactless restrictions (contact & contactless configured equally).

# this profile configures contactless equal to contact
# otherwise equal to cryptnox-default
name: contactless-no-sm
mode: developer-not-for-production
admin:
  key_ref: 9B
  mechanism: AES256
pin:
  min: 6
  max: 8
  retries: 6
  charset: numeric
puk:
  min: 8
  max: 8
  retries: 6
  charset: numeric
containers:
- oid: 5FC102
  name: chuid
  contact: ALWAYS
  contactless: ALWAYS
- oid: 5FC107
  name: ccc
  contact: ALWAYS
  contactless: ALWAYS
- oid: 5FC105
  name: auth-cert
  contact: ALWAYS
  contactless: ALWAYS
- oid: 5FC101
  name: card-auth-cert
  contact: ALWAYS
  contactless: ALWAYS
- oid: 5FC10A
  name: sign-cert
  contact: ALWAYS
  contactless: ALWAYS
- oid: 5FC10B
  name: keymgmt-cert
  contact: ALWAYS
  contactless: ALWAYS
- oid: 5FC106
  name: security-object
  contact: ALWAYS
  contactless: ALWAYS
- oid: 5FC103
  name: fingerprints
  contact: PIN
  contactless: PIN
- oid: 5FC108
  name: facial
  contact: PIN
  contactless: PIN
- oid: 5FC109
  name: printed
  contact: PIN
  contactless: PIN
keys:
- ref: 9B
  name: admin
  mechanism: AES256
  role: AUTHENTICATE
  contact: ALWAYS
  contactless: ALWAYS
  attributes:
  - PERMIT_EXTERNAL
  - PERMIT_MUTUAL
  - IMPORTABLE
- ref: 9A
  name: auth
  mechanism: ECCP256
  role: AUTHENTICATE
  contact: PIN
  contactless: PIN
  attributes:
  - IMPORTABLE
- ref: 9C
  name: sign
  mechanism: ECCP256
  role: SIGN
  contact: PIN
  contactless: PIN
  attributes:
  - IMPORTABLE
- ref: 9D
  name: keymgmt
  mechanism: ECCP256
  role: KEY_ESTABLISH
  contact: PIN
  contactless: PIN
  attributes:
  - IMPORTABLE
- ref: 9E
  name: card-auth
  mechanism: ECCP256
  role: AUTHENTICATE
  contact: ALWAYS
  contactless: ALWAYS
  attributes:
  - IMPORTABLE

@episource
episource deleted the feature/piv-mgmt-key-algorithm-probe branch September 8, 2026 14:32
@framefilter framefilter mentioned this pull request Sep 21, 2026
framefilter added a commit that referenced this pull request Sep 21, 2026
* release: 0.10.0 — version bump, changelog, and release-doc updates

Section 2 of packaging/RELEASING.md, on a prep branch for review.

- Workspace version 0.9.0 -> 0.10.0 across the workspace field and all 54
  inter-crate path-dep pins; Cargo.lock regenerated. semver-checks confirms
  0.10.0 is the correct (required) bump: one break, keyroost-token2otp's
  PinFlag gained a field and is not #[non_exhaustive].
- CHANGELOG: assembled the five queued changelog.d fragments into
  ## [0.10.0] - 2026-09-20 — nix flake (#109), PIV slot self-test (#127),
  Token2 OTP fingerprint unlock (#130), PIV mgmt-key algorithm probe (#124),
  OTP PIN-material trace redaction (#131). metainfo derives cleanly.
- migration.html: documented that one library break.
- Semantic doc audit (every doc file, no sampling) fixes: README crate table
  (+keyroost-pivtest), PIV bullet (+ the piv test self-test), OTP bullet
  (+ fingerprint unlock); an otp.html fingerprint-unlock section; SECURITY.md
  scoped-deps (+ p384 / ed25519-dalek / x25519-dalek, confined to
  keyroost-pivtest); TODO.md current-work header; RELEASING.md library-crate
  count 16 -> 17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpyGDFxYw5DSD97HXBAc2q

* token2otp: seal PinFlag with #[non_exhaustive] while it's already breaking

PinFlag gained an fp_enable field this release (#130), which was a
breaking change because the struct was not #[non_exhaustive] — unlike the
other response types that were sealed in 0.9.0. Since 0.10.0 already
breaks PinFlag, sealing it in the same release means consumers absorb one
break instead of two: after this, later firmware flags can be added as
fields without breaking anyone. Only same-crate construction (parse())
uses a struct literal, and #[non_exhaustive] does not restrict that, so
nothing in the workspace changes. Migration note and changelog updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpyGDFxYw5DSD97HXBAc2q

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants