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
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
self-signed-cert signing, and profile key mechanisms.
- `perso generate-key --create-key-object`, the dev/eval fallback
`import-key` already had.
- `remote authenticate` and `remote inspect`: identify and probe the card
through the Cryptnox remote PIV service, with the card in the local reader.
The tool relays the commands the service sends under a fail-closed policy
that keeps every other card function out of reach, and reports what the
service asserted apart from what it verified locally. Adds the `websockets`
dependency and exit codes 12 to 16.
- `remote reset` and `remote dev-reset`: wipe and reinstall the PIV function
through the service, behind the same irreversibility gate as `factory piv
preperso finalize` (typed phrase or explicit flag, `--yes` not accepted),
with the card's PIV state, data objects, security-domain key versions and
neighbouring functions read locally before consent and again afterwards.
The relay policy for these operations admits card content management
through the card manager only, restricts a DELETE to the PIV instance,
package and security domain (refusing one it cannot read unless the
channel encrypts commands), and stops at the first failed authentication.
- `remote attest`: generate a key in a slot through the service and receive
its key-attestation certificate. The certificate is verified locally against
the pinned anchors and bound to the card's CPLC UID, the requested slot, the
trust model and the certificate read back from the card before success is
reported.
- Coverage measurement in CI: `pytest-cov` in the `dev` extra, a project-wide
floor from `fail_under`, and a tighter floor over the command modules. With
branch coverage enabled both floors compare the combined statement-and-branch
percentage, which sits below line coverage alone.
- Coverage measurement in CI (`pytest-cov` in the `dev` extra) with a
project-wide floor and a tighter one over the command modules, both on
combined statement-and-branch coverage.
Expand Down
20 changes: 20 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@ The DESFire EV3 mechanism that embeds per-read authentication data (UID, counter
MAC) into a file's NDEF content. EV3-only.
_Avoid_: SUN (the message SDM produces, not the mechanism)

### Remote service

**Remote service**:
The Cryptnox-operated service that performs PIV lifecycle operations needing keys
that never leave Cryptnox, with the card in the holder's own reader. It drives; the
tool relays.
_Avoid_: cloud, backend, API (it is an operation partner, not a data source)

**Relay**:
The tool's role during a remote operation: transmit each command the service sends
to the card and return the answer verbatim. The relay never sends a command of its
own and never reassembles a chained response.
_Avoid_: proxy, tunnel (both suggest transparency; the relay refuses)

**Relay policy**:
The fail-closed allow-list the relay applies to every command before it reaches the
card: which applet may be selected, which instructions may pass, how many
authentication attempts each security domain may see. A refusal ends the operation.
_Avoid_: firewall, filter (a filter drops; the policy stops)

### Tool behavior

**Dry run**:
Expand Down
91 changes: 91 additions & 0 deletions docs/adr/0003-relay-for-the-remote-piv-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 3. A policed relay for the remote PIV service

## Context

Some PIV lifecycle operations need keys that never leave Cryptnox: the key that
makes a card genuine, and the certificate authority that attests keys generated
on it. Cryptnox operates a service that performs those operations with the card
in the holder's own reader. The service chooses every card command; the tool's
part is to carry commands to the card and answers back over a TLS WebSocket.

The card carries more than the PIV function. A wallet, FIDO2 passkeys, a
genuineness key and DESFire share the plastic, and each has retry counters or
content that a stray command can destroy. A card-management authentication that
fails spends one of a bounded number of retries, after which the card manager
locks for good. The service is one party; anything that can speak its protocol
to the tool is another.

The tool had no network code, and every command it sent to a card was one it had
composed itself.

## Options

1. **Present the service as a card connection**, so the existing PIV code runs
over the wire. Backwards: the card is local and the service drives, so the
tool would be relaying the service's commands whatever the seam is called,
and the CLI's transport reassembles chained responses the service expects to
drive itself.
2. **A transparent relay**: carry every command the service sends. Simplest, and
it hands the other card functions to whoever holds the connection.
3. **A relay behind a fail-closed policy**, per operation, over what a command
header reveals.

## Decision

Option 3.

The relay transmits each command the service sends through the raw card
connection and returns the answer verbatim, status word included. It never sends
a command of its own and never reassembles a chained response; the service sees
the card as a local reader would.

Before a command reaches the card, a policy decides. The policy is an allow-list
per operation: which applet may be selected (the PIV applet and its security
domain; the card manager only where card content management is the point),
which instructions may pass in each of those contexts, how many INITIALIZE
UPDATE and EXTERNAL AUTHENTICATE commands each security domain may see, how many
commands and seconds an operation may take. A DELETE may name the PIV instance,
its package or its security domain and nothing else; one whose target cannot be
read is refused unless the channel is known to encrypt command data. Key
generation is bound to the slot the holder asked for, and the management-key
handshake to key 9B, so no slot key can be exercised. The certificate write is
not bound: PUT DATA names its object in the data, not the header, so during an
attestation the service can write any PIV data object, and only the certificate
the tool reads back is checked. Card-holder verifier commands are never relayed. A refusal ends the operation and names the
command header, so the rules can be reviewed against what the service actually
needs rather than loosened in advance.

Inside an encrypted channel only the class and instruction bytes stay in the
clear. The policy fences which security domain the channel was opened to and
which instructions pass through it; it cannot see what an encrypted command
carries, and the documentation says so. The selected applet is tracked from the
card's answers, not the service's requests; an answer that leaves the selection
uncertain admits nothing but another SELECT.

The service's result is shown as the service's assertion. What the tool can
verify itself, it reads from the card before and after: identity, personalization
state, security-domain key versions, the neighbouring functions, and for key
attestation the returned certificate bound to the card, the slot and the
certificate stored on it. An unchecked binding never counts as a passed one.

Irreversible operations sit behind the gate the codebase already uses for
`finalize` and `fido reset`: a typed phrase interactively, an explicit flag
otherwise, and no `--yes` bypass. The development access credential comes from
the environment, never from an option. The production endpoint is a constant;
an override reaches the read-only operations only.

## Consequences

* `websockets` is the tool's first network dependency. The synchronous client
keeps every command synchronous, as the rest of the tool is.
* A service change that needs a command the policy does not admit fails loudly
with the header, and needs a tool release to admit it. That is the intended
trade: the policy is written from what the service was observed to send, not
from what it might.
* Relayed commands are written to the transcript with their data masked unless
the instruction is a known read, the reverse of the tool's own transcript rule,
because the tool did not compose the command. `remote --full-transcript`
lifts that masking for troubleshooting.
* The tool cannot witness that a key was generated on the card. The output says
so next to every key attestation.
* The glossary gains the remote service, the relay and the relay policy.
4 changes: 4 additions & 0 deletions docs/cli-basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Top-level commands
* - ``report card`` / ``report piv`` / ``report mifare`` / ``report fido`` /
``report genuine`` / ``report full``
- Secret-safe JSON reports (``--out FILE``).
* - ``remote authenticate`` / ``remote inspect`` / ``remote attest`` /
``remote reset`` / ``remote dev-reset``
- Operations run through the Cryptnox remote PIV service, with the card
in the local reader.
* - ``shell``
- Interactive prompt: run subcommands without the ``cryptnox-id`` prefix
(type ``piv info``); global options passed when launching carry into every
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ dependencies = [
"cbor2>=5.4,<6",
"asn1crypto>=1.5",
"pyyaml>=6.0",
# The remote PIV service speaks WebSocket. Floor is 13.0, the first release
# whose synchronous client (websockets.sync.client) is stable; the CLI is
# synchronous throughout and does not run an event loop.
"websockets>=13,<18",
]

[project.urls]
Expand Down
Loading
Loading