Skip to content

Fall back to the PIV management key when the admin channel truncates a generated key - #17

Merged
mmlado merged 4 commits into
mainfrom
fix/generate-key-truncated-template
Oct 7, 2026
Merged

mmlado merged 4 commits into
mainfrom
fix/generate-key-truncated-template

Conversation

@mmlado

@mmlado mmlado commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

piv perso generate-key and piv quickstart fail with TLV value extends beyond buffer on cards whose applet does not chain responses on the secured path. RSA public-key templates are larger than one 256-byte response (270 bytes for RSA-2048), and such a card returns the first 256 bytes with SW 9000 and drops the rest.

The CLI now detects that response and completes the generation over plain APDUs after authenticating the PIV management key (9B), where the card chains normally. Cards that already return the full template follow exactly the same path as before.

Changes

  • Detection. RSA mechanism, SW 9000, exactly 256 bytes, and a leading 7F49 header that declares more than arrived. It cannot fire for ECC or for a card that chains.
  • Fallback. Plain SELECT, mutual authentication of 9B (GENERAL AUTHENTICATE, AES), then GENERATE as a plain APDU. The transport's existing 61xx handling reassembles the template.
  • factory piv preperso set-mgmt-key, new. Loads the 9B value over the admin channel. Refuses to overwrite a value that is set unless --replace is given, and never creates a missing 9B object.
  • factory piv preperso status reports whether 9B exists and holds a value.
  • PIV_MGMT_KEY, new environment variable (hex, 16/24/32 bytes). --default-keys also supplies the published test value for 9B. Never read from the command line; registered with the redactor.
  • JSON. generate-key gains generate_path and management_key; the quickstart generate-key step carries the same two fields. Additive.
  • Errors. Management-key failures exit with code 7 and name the stage. A template that still cannot be parsed is a CLI error naming the response length, not a traceback.
  • Docs. Command reference, personalization guide, exit codes, glossary, and ADR 0002 recording the decision.
  • scripts/test-local.sh, separate commit: runs the CI jobs locally in Docker.

Testing

Local: ruff, ruff format, mypy, 433 unit tests (86 new), Sphinx with -W, and the package build all pass.

On hardware, contact reader:

Card Result
Applet that truncates, SCP02 RSA-2048 detected the 256-byte response, fell back, and returned the full key. ECC P-256 stayed on the admin channel. status reported the empty 9B before set-mgmt-key and a value after.
Applet that chains, SCP02, personalized before this change with no 9B value RSA-2048 on the admin channel. The APDU transcript shows no GENERAL AUTHENTICATE addressed to 9B and no extra SELECT.
Applet that chains, SCP03 RSA-2048 on the admin channel, full template.
9B created without PERMIT_MUTUAL The fallback stops with exit code 7 and names the missing attribute.

Not covered

  • The fallback over SCP03. No SCP03 card running a truncating applet was available. The management-key authentication and the plain GENERATE both run outside the secure channel, so the SCP version does not reach them, but that is not a hardware test.
  • RSA-3072 and RSA-4096 through the fallback.

🤖 Generated with Claude Code

mmlado and others added 2 commits September 16, 2026 18:56
…a generated key

On-card RSA generation returns a 7F49 template larger than one 256-byte
response. Cards that do not chain on the secured response path deliver the
first 256 bytes with a plain success status and drop the rest, so the CLI
parsed a partial template and failed with a raw TLV error.

Detect that response and repeat the generation over plain APDUs behind a
mutual PIV management key (9B) authentication, where the card does chain.
Detection requires an RSA mechanism, SW 9000, exactly 256 bytes, and a
leading 7F49 header declaring more, so it cannot fire for ECC or for a card
that chains. Cards that return the full template are untouched.

Add factory piv preperso set-mgmt-key to load the 9B value, since the
profiles create the key object but never gave it one, and report 9B in
factory piv preperso status. The value comes from --default-keys or
PIV_MGMT_KEY, never the command line.

A public-key template the CLI still cannot parse now ends in a CLI error
naming the response length instead of an unhandled exception.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
scripts/test-local.sh runs lint, typecheck, the pytest matrix, the package
build and the HTML docs build in python:X-slim containers, matching
.github/workflows/ci.yml and docs.yml. The default pytest matrix covers every
Python version the package declares, 3.10 through 3.14, which is wider than
CI. Each job works on a copy of the repository that leaves .git and ignored
paths behind, so nothing is written to the checkout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread src/cryptnox_id_cli/applets/piv/mgmt_auth.py Dismissed
Comment thread src/cryptnox_id_cli/applets/piv/mgmt_auth.py Dismissed
@mmlado
mmlado requested a review from embarquech September 25, 2026 15:33
@mmlado mmlado self-assigned this Sep 29, 2026
mmlado and others added 2 commits September 29, 2026 17:30
Conflicts in CHANGELOG.md and tests/unit/test_redaction.py, both additive:
each side appended to the same place. Every line from both sides is kept,
main's entries and tests first and this branch's after them. The only line
that appears once instead of twice is the Fixed heading both sides opened.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A PIV_MGMT_KEY or PIV_SCP03_* value that was not valid hex raised the hex
parser's own ValueError, which quotes its input and is not a CLI error, so
one mistyped digit printed the rest of the key in a traceback. Both
resolvers now raise SecretInputError naming the variable, without the value
and without the parser's exception chained to it.

generate-key and the quickstart generate-key step report the management-key
authentication as management_key_auth instead of management_key. factory piv
preperso status already uses management_key for a different object (present,
mechanism, value_set, sw), and one name should not carry two shapes.

Document that --default-keys supplies the management key value as well, so
PIV_MGMT_KEY is not read when it is given.

Add a quickstart test that runs through the fallback into the certificate
step against a card model that tracks the secure channel and the 9B role,
so a step that does not reopen the admin channel fails. Sort the new
NO_DRY_RUN entry, and say in scripts/test-local.sh that the SELinux label is
the one change the script makes to the checkout.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mmlado
mmlado merged commit f962983 into main Oct 7, 2026
12 checks passed
@mmlado
mmlado deleted the fix/generate-key-truncated-template branch October 7, 2026 16:24
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