Repository navigation
Conversation
The remote PIV service performs lifecycle operations that need keys which never leave Cryptnox, with the card in the holder's own reader. This adds the client side for its two key-free operations. `remote authenticate` identifies the card to the service (ATR and CPLC UID); `remote inspect` probes whether the PIV applet and its security domain are present and which key versions they carry. Both open the local reader first, connect over TLS, answer the service's proof-of-work, and relay the commands the service sends. The relay is raw: it never sends a command of its own and never reassembles a chained response, so the service sees the card as a local reader would. A fail-closed relay policy decides what reaches the card. It is an allow-list per operation over the selected applet, class, instruction and SELECT target: the wallet, FIDO2, genuineness and DESFire functions cannot be selected, card-holder verifier commands are never relayed, and EXTERNAL AUTHENTICATE is refused for both operations so no card-management retry can be spent. A refusal ends the operation and names the command header. The output keeps what the service asserted apart from what was verified locally (CPLC UID and ATR read before the connection opens). Relayed commands are written to the transcript with their data masked unless the instruction is a known read; `remote --full-transcript` lifts that extra masking. `websockets` becomes a runtime dependency (synchronous client). New exit codes 12 to 16 cover connection, protocol, policy and service failures. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The remaining three operations of the remote PIV service, on the relay and policy the read-only operations introduced. `remote reset` wipes and reinstalls the PIV function: the service authenticates to the card manager, deletes the PIV applet, its package and its security domain, loads the applet again, recreates the security domain and loads the card's keys. `remote dev-reset` does the same for development cards and leaves the card manager and the security domain on the public default key; its access credential comes from the environment only. Both sit behind the irreversibility gate the codebase already uses: a typed phrase interactively, an explicit flag otherwise, and no `--yes` bypass. What the card holds is read locally and shown before consent, and read again afterwards next to the service's result. The relay policy for these operations admits card content management through the card manager only, lets a DELETE whose data travels in the clear name nothing but the PIV instance, package and security domain, caps INITIALIZE UPDATE, EXTERNAL AUTHENTICATE and LOAD per operation, and stops at the first failed authentication so no further attempt can spend one of the card's bounded card-management retries. Inside an encrypted channel the policy sees instructions, not targets; the docs say so. `remote attest` generates a key in one slot through the service and receives its key-attestation certificate. The policy binds key generation to the requested slot and the management-key handshake to key 9B, so no slot key can be exercised. The certificate is then verified locally: chain to the pinned anchors, subject serial number against the card's CPLC UID, attested slot against the request, trust model, and the certified key against the certificate read back from the card. An unchecked binding never counts as a passed one, and the output states that on-card generation is the service's claim. `inspect` also learned that the service probes the applet's own admin channel with INITIALIZE UPDATE, and admits that probe. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Both rules were written from the protocol description before the operations had been run against the service. Running them showed each rule refusing a command the service legitimately needs, so each refusal is now narrower while the protection it exists for is unchanged. The secure-channel authentication caps were counted per operation, but the service opens a fresh card-manager channel for each step of a reset: delete, load, install, create the security domain, key it, extradite, load the second key version, verify. A reset was refused after the security domain had been keyed, with the applet installed but not yet extradited. The caps are raised to sixteen for both INITIALIZE UPDATE and EXTERNAL AUTHENTICATE. They were never the real protection against a spent retry: that is the abort on the first authentication the card rejects, which is untouched. The service sets the PIV management key with CHANGE REFERENCE DATA, key reference 9B, inside the applet's secure channel. The policy refused the whole instruction as a card-holder verifier, which is true of the PIN and the PUK but not of the management key: 9B carries no card-holder retry counter, and the service loads it before generating a key. CHANGE REFERENCE DATA is now allowed for key reference 9B alone, and only in operations whose rules opt in, which are reset, dev-reset and attest. The PIN and the PUK stay refused everywhere, as do VERIFY and RESET RETRY COUNTER whatever they address. Attest's EXTERNAL AUTHENTICATE cap goes from two to four for the same reason as the reset caps. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The service authenticates to the card manager with one of two key tiers: the card's own key, derived per card from its identity, or the publicly known GlobalPlatform default. Until now the command took `--fused` to select the derived key and sent the default otherwise, so the safe production case was the one the caller had to remember to ask for. The two mistakes are not symmetric. Forgetting the flag on a development card costs a failed authentication and nothing else; forgetting it on a production card presents a published key to a card that should only ever answer to its own. Defaults should fail safe, so the flag is inverted: the derived key is the default, and `--default-keys` opts into the development path. The name matches the flag the rest of the tool already uses for the publicly known GlobalPlatform test keys. The parameter is sent to the service explicitly either way, so the command's behaviour does not depend on which default the service applies when it is absent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Six findings on the pull request, none of them a defect, each made unambiguous rather than suppressed. The FrameChannel protocol methods carried `...` bodies, which the analyser reads as statements without effect; they now carry docstrings, which also document the contract. The empty `except` in the certificate pre-read has the comment explaining why an unreadable applet is not an error there. In the tests, a helper named `delete` built a DELETE APDU and was called inside an `assert`, which the analyser flags as a possible side effect; it is `delete_apdu` now. A test that raised inside a `with pytest.raises` block looked to the analyser as if the assertion after it could never run; the raise moved into a nested function so the flow is plain. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The guide, the command reference and the sections added to the README and to the exit-codes, JSON-output and troubleshooting pages leave this branch. They follow in a separate change, reviewed on its own, based on this one. The command map in cli-basics keeps a row naming the five commands, which is what the command-reference drift guard requires of any change that adds a command; the cross-reference to the command reference page goes with the page. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the
remotecommand group: operations carried out by the Cryptnox remote PIV service with the card in the local reader.remote authenticateandremote inspect: identify and probe the card through the service, key-free.remote attest: generate a key in a slot through the service and receive its key-attestation certificate, verified locally before success is reported.remote resetandremote dev-reset: wipe and reinstall the PIV function, behind the irreversibility gate the codebase already uses. The service authenticates with the card's own derived key by default;--default-keysselects the publicly known GlobalPlatform key for development cards, matching the flag the rest of the tool uses for that purpose.The tool is a relay: it transmits each command the service sends and returns the card's answer verbatim, never composing a command of its own and never reassembling a chained response. A fail-closed policy decides, per operation, what reaches the card: which applet may be selected, which instructions may pass in each context, how many authentication attempts each security domain may see, and (where the data is readable) what a DELETE may name. The wallet, FIDO2, genuineness and DESFire functions are unreachable through it. A refusal ends the operation and names the command header.
What the service asserts and what the tool verified locally are shown apart, in the human output and in
--json.Design
docs/adr/0003-relay-for-the-remote-piv-service.mdrecords the decision and the options not taken. The guide (docs/remote/remote-guide.rst) states the policy's limits plainly: inside an encrypted channel it sees instructions, not targets, and on-card key generation is the service's claim.Changes
src/cryptnox_id_cli/remote/: frame codec, proof-of-work solver, WebSocket channel (TLS, verifying), relay loop, relay policy, attestation post-verification.cli/commands/remote.py; registered inmain.py, classified indryrun.py.RemoteErrorfamily intransport/errors.py, exit codes 12 to 16, documented indocs/exit-codes.rst.AppContext.apdu_trace: the transcript sink for code that transmits outside aCardSession.websockets>=13,<18as a runtime dependency (synchronous client; the tool stays synchronous).docs/remote/(guide, commands),cli-basics,json-output,troubleshooting, README, CHANGELOG, glossary terms inCONTEXT.md.MockConnection, a generated test PKI for the attestation checks, and adversarial policy cases (other card functions, verifier commands, logical channels, DESFire and CTAP framing, DELETE of non-PIV content, second authentication attempt, oversize and malformed frames).Verification
-W: green locally, and the suite also passes on Python 3.10 in a container (CI's other matrix leg).authenticateandinspectsucceed; the service's picture of each card matches what the tool reads locally.resetsucceeds: all seven steps report success, and the tool's own post-check confirms independently that the PIV applet is present and the security domain came back carrying a second key version where it had only one before.attestsucceeds: a key is generated in the slot and the returned certificate passes every local check — chain to the pinned trust anchor, subject serial number against the card's own identity, attested slot against the request, trust model, and the certified key against the certificate read back from the card.dev-resetreaches the service and is refused for want of a valid credential, which exercises its error path.890ddf1, each refusal having stopped the operation cleanly and named the offending command header, with the card left recoverable. That is the layer working as intended.What the live runs settled
Not in this change
websocketshidden import; the spec is already stale for other reasons and gets its own change.channel.py.🤖 Generated with Claude Code