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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `piv inventory`: the applet's key objects, verifiers, containers and config
flags with their access rules, read without authentication.
- The finalized (SECURED) state, read from the applet's status object, in
`piv status`, `info`, `report` and `factory piv preperso status`.
- `RSA4096` in `perso generate-key`, `perso import-key`, CSR and
self-signed-cert signing, and profile key mechanisms.
- `perso generate-key --create-key-object`, the dev/eval fallback
Expand All @@ -21,13 +25,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
`factory piv preperso status` reports whether 9B holds a value.
- `PIV_MGMT_KEY` (hex) for the PIV management key, next to `PIV_SCP03_*`.

### Changed

- `piv admin status` and `factory piv preperso status` read the admin security
domain's SCP version and key versions from its key information template
instead of sending INITIALIZE UPDATE, which this chip counts as a failed
authentication. `piv admin status --key-version` is gone.

### Removed

- `transport.pcsc.list_reader_names()`; `reader_states()` returns the same
names with card presence and ATR.

### Fixed

- `factory piv preperso status` reported the SCP version of whatever applet was
selected last, not the PIV security domain's.

- `factory piv preperso load-config` warns that structural operations are
permanent (existing elements cannot be changed or removed; only new ones can
be added) instead of calling them reversible before finalize.
Expand Down
11 changes: 9 additions & 2 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@ data objects. It decides what _does_ exist.
_Avoid_: provisioning, enrollment

**Personalization state**:
A ladder, never a boolean: pre-personalized, partially personalized, personalized,
secured (locked). Output state labels follow the ladder.
A ladder, never a boolean: pre-personalized, partially personalized, personalized.
Output state labels follow the ladder. Finalized (SECURED) is reported beside the
ladder, not as a rung: finalize locks the structure only, so a secured applet can
sit at any rung.
_Avoid_: initialized, blank, done

**Inventory**:
The applet's own listing of its structure - key objects, verifiers, containers and
config flags with their access rules - read without authentication.
_Avoid_: dump, snapshot (export-config is a different, observed-state file)

**Profile**:
A named pre-personalization configuration, such as `cryptnox-default` or `ms-logon`.
_Avoid_: template, preset
Expand Down
7 changes: 7 additions & 0 deletions docs/factory/factory-commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ manufacturing procedure outside this documentation.
factory piv preperso set-mgmt-key load the PIV management key (9B) value
factory piv preperso finalize IRREVERSIBLY lock the applet structure

``status`` never authenticates: the finalize (SECURED) state comes from the
applet's own status object, and the admin security domain's SCP version and key
versions from its key information template. On this chip an INITIALIZE UPDATE
that is not followed by a successful EXTERNAL AUTHENTICATE counts as a failed
authentication, so a read-only status must not send one. ``piv inventory`` lists
the structure itself.

``load-config`` sends one structural operation per SCP03 session (a platform
requirement of this card), so a profile load is a sequence of short commands;
it stops at the first rejection and reports exactly what was applied.
Expand Down
15 changes: 14 additions & 1 deletion docs/piv/piv-commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,17 @@ Inspection
piv status lifecycle state, PIN/PUK status, object presence
piv discover the Discovery object (PIN usage policy)
piv slots key slots and which hold a certificate
piv inventory key objects, verifiers, containers and config, with access rules
piv validate consistency check (NOT a NIST/FIPS validation)
piv quickstart one-shot personalization (see the PIV quick start)

``piv inventory`` is the applet's own listing of its structure, read without
authentication: every key object with its mechanism, roles, attributes and access
modes, every verifier with its length and retry limits, every container with its
access modes, plus the applet version, its finalize state and its config flags.
An applet whose ``restrict-enumeration`` flag is set withholds the structure
(``6986``) and the command says so; version, state and config still answer.

PIN
---

Expand Down Expand Up @@ -65,9 +73,14 @@ Admin channel

.. code-block:: text

piv admin status probe the SCP03 channel (read-only)
piv admin status the admin security domain: SCP version, key versions (no auth)
piv admin authenticate open the channel (mutual auth) + harmless self-test

``piv admin status`` selects the PIV security domain and reads its key information
template. It never sends INITIALIZE UPDATE: on this chip an INITIALIZE UPDATE that is
not followed by a successful EXTERNAL AUTHENTICATE counts as a failed authentication,
and a status read must not spend one.

Certificates and objects
------------------------

Expand Down
4 changes: 4 additions & 0 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ The slot's key has the wrong role. Signing needs a **SIGN-role** slot such as
default-profile 9A is AUTHENTICATE-only and refuses ``GENERAL AUTHENTICATE``
signing. See :doc:`/factory/pre-personalization-profiles`.

**``6986`` on ``piv inventory``.**
The applet's ``restrict-enumeration`` config flag is set, so the structure
listing needs the admin role. Version, state and config are still readable.

**``0xAE AUTHENTICATION_ERROR`` on ``mifare app delete``.**
Deleting an application requires authentication. Pass ``--zero-key`` (factory
default) or ``--key-env NAME``; the CLI then authenticates and sends a MACed
Expand Down
28 changes: 2 additions & 26 deletions src/cryptnox_id_cli/applets/piv/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@

def scp_label(value: object) -> str:
"""Human-readable SCP version label. ``value`` is untyped at the call sites
(loosely-typed probe dicts, optional session attributes), so narrow here
instead of duplicating the isinstance check at every caller."""
(optional session attributes, inferred versions), so narrow here instead of
duplicating the isinstance check at every caller."""
return _SCP_LABELS.get(value, "unknown") if isinstance(value, int) else "unknown"


Expand All @@ -63,30 +63,6 @@ def select(self) -> None:
if not resp.ok:
raise Scp03Error(f"SELECT PIV failed (SW={resp.sw_hex()}).")

def initialize_update_probe(self, key_version: int = 0) -> dict[str, object]:
"""Read-only probe: INITIALIZE UPDATE only (auth not completed).

Reports the SCP version from ``keyInfo[1]`` so the caller can tell an SCP02 card
(28-byte body) from an SCP03 one (29/32-byte body)."""
resp = self.card.transmit(
APDU(0x80, 0x50, key_version, 0x00, data=bytes(8), le=256),
context="INITIALIZE UPDATE (probe)",
)
if not resp.ok:
raise Scp03Error(f"INITIALIZE UPDATE not supported / failed (SW={resp.sw_hex()}).")
b = resp.data
scp_id = b[11] if len(b) > 11 else None
# SCP03 puts the i-param at body[12]; SCP02 body[12:14] is the sequence counter.
scp_i = b[12] if (scp_id == SCP03 and len(b) > 12) else None
return {
"supported": (scp_id == SCP03 and len(b) >= 29) or (scp_id == SCP02 and len(b) >= 28),
"scp_version": scp_id,
"key_version": b[10] if len(b) > 10 else None,
"scp_id": scp_id,
"scp_i": scp_i,
"key_diversification": b[0:10].hex().upper(),
}

def open(
self,
keys: Scp03Keys | Scp02Keys,
Expand Down
Loading
Loading