Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- `RSA4096` alongside the existing algorithms: `perso generate-key`,
`perso import-key`, CSR/self-signed-cert signing, and profile key
mechanisms.
- `perso generate-key --create-key-object` - the same dev/eval fallback
`import-key` already had.

## [1.0.3] - 2026-08-31

### Changed
Expand Down
2 changes: 1 addition & 1 deletion docs/factory/pre-personalization-profiles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ Profile YAML
attributes: [IMPORTABLE]

**Mechanisms**: ``AES128``, ``AES192``, ``AES256``, ``RSA2048``, ``RSA3072``,
``ECCP256``, ``ECCP384``, ``CS2``, ``CS7``.
``RSA4096``, ``ECCP256``, ``ECCP384``, ``CS2``, ``CS7``.

**Access modes** (used for ``contact``/``contactless`` on both containers and
keys): ``ALWAYS``, ``NEVER``, ``PIN``, ``VCI`` (contact-interface-only, no
Expand Down
4 changes: 2 additions & 2 deletions docs/piv/piv-personalization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ mechanism the slot has no object for returns ``6A80``.
ECC P-256 everywhere, accepts explicit RSA only for ``--profile ms-logon`` on
9A (the one built-in shape with an RSA object, and the one Windows needs —
the ECC default warns there), and elsewhere refuses RSA early rather than
failing on the card. RSA-1024 and
RSA-4096 are removed from the build; the CLI never offers them.
failing on the card. RSA-1024 is removed from the build; the CLI never offers
it.

.. _piv-personalization-import-key:

Expand Down
9 changes: 3 additions & 6 deletions src/cryptnox_id_cli/applets/piv/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,9 @@
0x2E: "PIV-SM-CS7 (AES-256/SHA-384)",
}

# Algorithms this OpenFIPS201 FIPS build actually implements. RSA-4096 (0x16) is
# included: on-card createKey + GENERATE for RSA-4096 is confirmed on the D600/SCP03
# hardware (returns a real 512-byte modulus), gated only by the chip's DF2B max-RSA
# = 0x1000 on a non-fused card. NOTE: this covers on-card key GENERATION and key
# objects; the host-side key-IMPORT/sign helpers (keyimport.py) still cover only
# RSA-2048/3072, so external RSA-4096 import is a separate, not-yet-implemented path.
# Algorithms this OpenFIPS201 FIPS build actually implements. A chip whose DF2B
# configuration caps RSA below a requested size answers 6F00; the CLI maps that to
# a size-cap message on GENERATE/import.
SUPPORTED_ALGORITHMS: frozenset[int] = frozenset(
{0x07, 0x05, 0x16, 0x08, 0x0A, 0x0C, 0x11, 0x14, 0x27, 0x2E}
)
Expand Down
6 changes: 3 additions & 3 deletions src/cryptnox_id_cli/applets/piv/keyimport.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
# Mechanism -> (curve, scalar/point sizes) for ECC, modulus size for RSA.
_EC_BY_MECH: dict[int, type[ec.EllipticCurve]] = {0x11: ec.SECP256R1, 0x14: ec.SECP384R1}
_EC_SCALAR_LEN = {0x11: 32, 0x14: 48}
_RSA_MODULUS_LEN = {0x07: 256, 0x05: 384}
_RSA_MODULUS_LEN = {0x07: 256, 0x05: 384, 0x16: 512}

# Host-side slot policy (SP 800-78 per-key-reference algorithm table): PKI slots
# and the retired key history accept the four asymmetric mechanisms; 9B is
Expand Down Expand Up @@ -133,11 +133,11 @@ def mechanism_for_key(key) -> int:
if key.key_size == k * 8:
return mech
raise KeyImportError(
f"RSA-{key.key_size} not supported; this applet takes RSA-2048 or RSA-3072."
f"RSA-{key.key_size} not supported; this applet takes RSA-2048, RSA-3072 or RSA-4096."
)
raise KeyImportError(
f"unsupported key type {type(key).__name__}; this applet takes "
"ECC P-256/P-384 and RSA-2048/3072."
"ECC P-256/P-384 and RSA-2048/3072/4096."
)


Expand Down
2 changes: 1 addition & 1 deletion src/cryptnox_id_cli/applets/piv/perso.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
_EC_CURVES = {0x11: ec.SECP256R1, 0x14: ec.SECP384R1}

# Asymmetric algorithm name -> mechanism id (those this applet supports).
ALGORITHMS = {"ECCP256": 0x11, "ECCP384": 0x14, "RSA2048": 0x07, "RSA3072": 0x05}
ALGORITHMS = {"ECCP256": 0x11, "ECCP384": 0x14, "RSA2048": 0x07, "RSA3072": 0x05, "RSA4096": 0x16}


def pad_pin(value: bytes, length: int = 8) -> bytes:
Expand Down
1 change: 1 addition & 0 deletions src/cryptnox_id_cli/applets/piv/profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"AES256": 0x0C,
"RSA2048": 0x07,
"RSA3072": 0x05,
"RSA4096": 0x16,
"ECCP256": 0x11,
"ECCP384": 0x14,
"CS2": 0x27,
Expand Down
35 changes: 31 additions & 4 deletions src/cryptnox_id_cli/cli/commands/piv.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,7 +805,10 @@ def perso() -> None:
@perso.command("generate-key")
@click.option("--slot", required=True, help="Key slot hex, e.g. 9A.")
@click.option(
"--algorithm", default="ECCP256", show_default=True, help="ECCP256/ECCP384/RSA2048/RSA3072."
"--algorithm",
default="ECCP256",
show_default=True,
help="ECCP256/ECCP384/RSA2048/RSA3072/RSA4096.",
)
@click.option(
"--default-keys",
Expand All @@ -816,9 +819,21 @@ def perso() -> None:
@click.option(
"--out", "out_", type=click.Path(dir_okay=False), help="Write public-key PEM to FILE."
)
@click.option(
"--create-key-object",
is_flag=True,
help="If the slot has no key object for this (slot, algorithm) pair, create one "
"first (PUT DATA ADMIN, non-finalized cards only) - same dev/eval fallback as "
"`import-key --create-key-object`.",
)
@click.pass_obj
def perso_generate_key(
app: AppContext, slot: str, algorithm: str, default_keys: bool, out_: str | None
app: AppContext,
slot: str,
algorithm: str,
default_keys: bool,
out_: str | None,
create_key_object: bool,
) -> None:
"""Generate a key pair ON-CARD; the private key never leaves the card.

Expand Down Expand Up @@ -850,7 +865,13 @@ def perso_generate_key(
f"Replace the certified key in slot {slot}?", default=False
):
raise click.Abort()
public_key = _generate_key_on_card(PivAdmin(session), keys, ref, mech, label=slot)
adm = PivAdmin(session)
if create_key_object:
_select(session)
probe = session.transmit(keyimport.probe_apdu(ref, mech), context=f"probe {slot}")
if probe.sw == 0x6A86:
_create_key_object(app, adm, keys, ref, mech, False, slot)
public_key = _generate_key_on_card(adm, keys, ref, mech, label=slot)
pem = perso_mod.public_key_pem(public_key)
der = public_key.public_bytes(
serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo
Expand Down Expand Up @@ -890,6 +911,8 @@ def _generate_key_on_card(adm: PivAdmin, keys, ref: int, mech: int, *, label: st
adm.select()
adm.open(keys)
resp = adm.send(perso_mod.generate_keypair_apdu(ref, mech), context=f"GENERATE {label}")
if resp.sw == 0x6F00 and keyimport.rsa_modulus_len(mech):
raise CryptnoxError(f"GENERATE key {label} failed (SW=6F00): {_IMPORT_SW_HINTS[0x6F00]}.")
if not resp.ok:
raise StatusWordError(resp.sw1, resp.sw2, context=f"GENERATE key {label}")
return perso_mod.parse_public_key(mech, resp.data)
Expand Down Expand Up @@ -994,6 +1017,10 @@ def signer(digest: bytes) -> bytes:
"CRT attribute does not match; try the other --rsa-form"
),
0x6700: "element too long for the transport (chaining should have engaged - report this)",
0x6F00: (
"the chip refuses this key size - its pre-issuance maximum-RSA configuration "
"(DF2B) caps RSA below it, and that is factory-set before the card is secured"
),
}


Expand Down Expand Up @@ -2130,7 +2157,7 @@ def _collect_quickstart_facts(session, ref: int, mech: int) -> tuple[CardFacts,
"--algorithm",
default="ECCP256",
show_default=True,
help="ECCP256, ECCP384, RSA2048 or RSA3072; the slot must have a key object for it "
help="ECCP256, ECCP384, RSA2048, RSA3072 or RSA4096; the slot must have a key object for it "
"(RSA: only ms-logon's 9A among the built-in profiles — pass RSA2048 explicitly "
"for Windows, which does not enumerate EC keys).",
)
Expand Down
1 change: 1 addition & 0 deletions src/cryptnox_id_cli/crypto/csr.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
0x14: (hashlib.sha384, "sha384_ecdsa", 48),
0x07: (hashlib.sha256, "sha256_rsa", None),
0x05: (hashlib.sha256, "sha256_rsa", None),
0x16: (hashlib.sha256, "sha256_rsa", None),
}

_RDN = {
Expand Down
Loading