From a7be40c7a96e52ed8ad2d428863ac350934ef123 Mon Sep 17 00:00:00 2001 From: Mladen Milankovic Date: Wed, 7 Oct 2026 12:53:57 +0200 Subject: [PATCH 1/3] Document the remote operations Guide and command reference for the remote command group, with the rows for exit codes 12 to 16, the remote sections of the JSON-output and troubleshooting pages, the README scenario, and the cross-reference from the command map. Co-Authored-By: Claude Fable 5.1 --- README.md | 12 ++++ docs/exit-codes.rst | 20 ++++++ docs/index.rst | 7 ++ docs/json-output.rst | 14 ++++ docs/remote/remote-commands.rst | 117 +++++++++++++++++++++++++++++++ docs/remote/remote-guide.rst | 118 ++++++++++++++++++++++++++++++++ docs/troubleshooting.rst | 28 ++++++++ 7 files changed, 316 insertions(+) create mode 100644 docs/remote/remote-commands.rst create mode 100644 docs/remote/remote-guide.rst diff --git a/README.md b/README.md index 5032cfe..b151d8a 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,18 @@ cryptnox-id doctor # PC/SC service, reader, per-function reachability cryptnox-id shell # run subcommands without re-typing the prefix ``` +### 6. Use the Cryptnox remote PIV service + +```bash +cryptnox-id remote inspect # what the service sees: applet, security domain, key versions +cryptnox-id remote attest --slot 9C # a key generated through the service, with its attestation +cryptnox-id remote reset # wipe and reinstall the PIV function (irreversible, gated) +``` + +The card stays in your reader; the tool relays the service's commands under a +policy that keeps the other card functions out of reach, and shows what the +service asserted apart from what it verified locally. + --- ## Documentation diff --git a/docs/exit-codes.rst b/docs/exit-codes.rst index 8a67096..662b6a9 100644 --- a/docs/exit-codes.rst +++ b/docs/exit-codes.rst @@ -51,3 +51,23 @@ machine token (see :doc:`/json-output`). * - 11 - ``ev2_error`` - DESFire secure-messaging (session/MAC) failure. + * - 12 + - ``remote_error`` + - A remote PIV service operation failed for a reason without a more + specific class. + * - 13 + - ``remote_connect`` + - The remote PIV service could not be reached, or the TLS handshake + failed. + * - 14 + - ``remote_protocol`` + - The remote PIV service sent something the client cannot interpret + safely. + * - 15 + - ``remote_policy`` + - The remote PIV service asked for a card command the relay policy + refuses to send. The message names the command header. + * - 16 + - ``remote_failed`` + - The exchange completed, but the remote PIV service reported the + operation as failed. diff --git a/docs/index.rst b/docs/index.rst index 8c3342e..8c1085e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -62,5 +62,12 @@ driven by one tool: |cli|. Guide Commands +.. toctree:: + :maxdepth: 2 + :caption: Remote service + + Guide + Commands + .. API Reference — RESERVED: autodoc of the Python package, if/when it is publicly pip-installable (see the internal documentation plan, decision D4). diff --git a/docs/json-output.rst b/docs/json-output.rst index 7415a0b..832fc1d 100644 --- a/docs/json-output.rst +++ b/docs/json-output.rst @@ -159,3 +159,17 @@ re-present the card, and check the reader passes native DESFire APDUs. A section may also report ``{"state": "Unknown", "error": ""}`` when a probe fails. Nothing in any report contains a PIN, key, or private material — ``safe_to_share`` is always ``true``. + +Remote service operations +------------------------- + +``remote`` commands return one object with a fixed top level: ``op``, +``endpoint``, ``card`` (reader, ATR, CPLC UID and serial, all read locally), +``result`` (the service's own result object, passed through unchanged), +``verified_locally`` (each cross-check as ``true``, ``false`` or ``null`` when +the service reported nothing to compare) and ``relay`` (commands relayed, +seconds, proof-of-work bits, the service's log lines). ``reset`` and +``dev-reset`` add ``before`` and ``after`` inventories; ``attest`` adds +``verification`` with every binding checked on the returned certificate. +Fields under ``result`` are the service's assertions; only ``verified_locally``, +``before``, ``after`` and ``verification`` are the tool's own findings. diff --git a/docs/remote/remote-commands.rst b/docs/remote/remote-commands.rst new file mode 100644 index 0000000..ebd8229 --- /dev/null +++ b/docs/remote/remote-commands.rst @@ -0,0 +1,117 @@ +Remote service commands +======================= + +The ``remote`` commands run operations through the Cryptnox remote PIV +service. The card stays in the local reader; |cli| relays the commands the +service sends to the card and reports the outcome. What the service asserts +and what |cli| verified locally are shown apart. The service, the relay policy +and what each operation does to the card are described in +:doc:`/remote/remote-guide`. + +Every command opens the local reader first, then connects to the service over +TLS and answers a small proof-of-work before any command is relayed. Only the +PIV function and its security domains are reachable through the relay; the +other card functions are refused by the relay policy. + +Group options +------------- + +.. code-block:: text + + remote [--full-transcript] COMMAND + + Options: + --full-transcript write relayed commands to --apdu-log and --verbose + without the extra masking the relay applies by + default; the standard secret masking still applies + +Identification and inspection +----------------------------- + +.. code-block:: text + + remote authenticate identify the card to the service: ATR and CPLC UID, + nothing else; key-free on both sides + + remote inspect probe the PIV function through the service: whether + the PIV applet and its security domain are present + and which key versions they carry; key-free, the + relay refuses any authentication attempt + +Both commands report the card as read locally (reader, ATR, CPLC UID), the +service's result as the service asserted it, and a cross-check of the fields +the two have in common. + +Key attestation +--------------- + +.. code-block:: text + + remote attest [--slot 9C] [--algorithm ECCP256] [--ca-obj HEX] [--out FILE] + + generate a key in the slot through the service and + receive its key-attestation certificate; the service + also stores the certificate in the card's attestation + container. The slot's previous key and the container's + previous certificate are replaced: when either holds + something, the command asks, or needs --yes. + + Options: + --slot HEX 9A, 9C, 9D, 9E or a retired slot 82 to 95 (default 9C) + --algorithm NAME ECCP256 (default), ECCP384, RSA2048, RSA3072, RSA4096 + --ca-obj HEX signing sub-CA in the service; omit for the + key-attestation CA + --out FILE write the returned certificate (PEM) + +The returned certificate is verified locally before success is reported: the +chain must end at a pinned trust anchor, the subject serial number must be the +card's CPLC UID, the ``cryptnoxPivSlot`` extension must name the requested +slot, the trust model must be the server-driven value, and the certified +public key must equal the certificate read back from the card's attestation +container. That the key was generated on the card is the service's claim; the +tool cannot witness it and says so. + +Reset +----- + +.. code-block:: text + + remote reset [--default-keys] [--i-understand-this-is-irreversible] + + IRREVERSIBLY wipe and reinstall the PIV function: the + service deletes the PIV applet, its package and its + security domain, reinstalls the applet, recreates the + security domain and loads the card's keys. Every PIV + key and certificate is destroyed. Interactively the + phrase RESET-PIV must be typed; non-interactively the + flag is required. --yes is not accepted. + + remote dev-reset [--default-keys] [--i-understand-this-is-irreversible] + + Development cards only. As reset, but the card manager + and the PIV security domain are left on the public + default key, so the PIV function can be worked on + locally. Needs the development access credential in + $CRYPTNOX_REMOTE_DEV_TOKEN (never an option). The + phrase is DEV-RESET-PIV. + + Options: + --default-keys development cards only: the service authenticates + with the publicly known GlobalPlatform default key + instead of deriving this card's own. A production + card refuses it, and the failed authentication costs + a card-management retry. + +Before asking for consent, both commands read and show what the card holds: +the PIV personalization state and data objects, the PIV security domain and +its key versions, and the state of the FIDO2 and genuineness functions, which +stay outside the operation. The same inventory is read again afterwards and +shown next to the service's result. + +Endpoint +-------- + +The service endpoint is fixed to ``wss://piv.cryptnox.com``. For development +against a local stand-in, ``CRYPTNOX_REMOTE_URL`` redirects the identification +and inspection commands only; it must be a ``wss://`` URL, or ``ws://`` to +loopback. ``reset`` and ``dev-reset`` refuse to run while it is set. diff --git a/docs/remote/remote-guide.rst b/docs/remote/remote-guide.rst new file mode 100644 index 0000000..ffacde7 --- /dev/null +++ b/docs/remote/remote-guide.rst @@ -0,0 +1,118 @@ +Remote service guide +==================== + +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. The remote PIV service holds those keys in a hardware +security module and performs the operation over a relayed command channel, +with the card in the holder's own reader. + +How a relayed operation works +----------------------------- + +1. |cli| opens the local reader and reads the card's ATR and CPLC serial. +2. It connects to the service over TLS and names the operation. +3. The service issues a small proof-of-work; |cli| answers it. This is an + anti-abuse throttle, not a credential. +4. The service sends card commands one at a time. |cli| checks each one + against the relay policy, transmits it, and returns the card's answer + verbatim, status word included. |cli| never sends a command of its own and + never reassembles a chained response; the service sees the card exactly as + a local reader would. +5. The service returns a result. |cli| shows it as the service's assertion and + adds what it verified locally. + +The relay policy +---------------- + +The card carries more than the PIV function. A relay that executed whatever +it was sent would expose the wallet, the FIDO2 passkeys and the genuineness +key to the service. The relay policy is an allow-list, per operation, over +what a command header reveals: the selected applet, the class byte, the +instruction, and for a SELECT the target application. Anything not listed is +refused, the operation stops, and the refusal names the command header. + +The policy also caps the number of commands and the time an operation may +take, limits secure-channel authentication attempts per security domain, and +never relays a card-holder PIN or PUK command. Inside a secure channel only +the class and instruction bytes stay in the clear, so 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. + +Operations +---------- + +``authenticate`` + Reads the ATR and CPLC serial. Key-free. Allows only card-manager reads. + +``inspect`` + Probes whether the PIV applet and its security domain are present and which + key versions they carry. Key-free: an authentication attempt from the + service is refused before it reaches the card. + +``attest`` + Generates a key in one slot and returns a key-attestation certificate for + it, signed by the Cryptnox key-attestation CA and stored in the card's + attestation container as well. The relay policy admits the applet's admin + channel for the key-object setup, the management-key handshake (never a + slot key), key generation for the requested slot only, and the certificate + write. The tool then verifies the certificate against the pinned anchors and + binds it to the card, the slot and the certificate stored on the card. The + generation itself happening on the card is the service's claim; nothing the + relay sees authenticates it. + +``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, then verifies the result. Every PIV key and certificate is destroyed; + the keys were never exportable, so nothing can bring them back. Interactive + use requires typing ``RESET-PIV``; non-interactive use requires + ``--i-understand-this-is-irreversible``. ``--yes`` is not accepted. + + The relay policy for this operation admits card content management through + the card manager only, lets a DELETE name nothing but the PIV instance, its + package and its security domain where the command travels in the clear, + caps authentication attempts per security domain, and stops at the first + failed authentication so no further attempt can spend a card-management + retry. Inside an encrypted channel the policy sees instructions, not + targets; that limit is real and is why the FIDO2 and genuineness state is + read before and after. + +``dev-reset`` + As ``reset``, for development cards, leaving the card manager and the PIV + security domain on the publicly documented GlobalPlatform key so the PIV + function can be pre-personalized and personalized locally. Anyone with a + reader can then install or delete applets on the card, and the card is not + genuine until it is reset through the service again. The development access + credential comes from ``CRYPTNOX_REMOTE_DEV_TOKEN`` only. + +The ``--default-keys`` flag +--------------------------- + +By default the service authenticates to the card manager with the card's own +key, derived per card from its identity. That is the state a card leaves the +factory in. A development card, or one deliberately put on the published +GlobalPlatform default key, needs ``--default-keys`` instead; without it the +service presents a derived key the card does not hold, the authentication +fails, and one of the card's bounded card-management retries is spent. The +flag is the caller's statement about the card, so the default is the safe one: +forgetting it on a development card costs a failed authentication and nothing +else, while the reverse mistake on a production card would present a published +key to a card that should only ever answer to its own. + +What is sent to the service +--------------------------- + +The card's ATR and CPLC serial identify the card to the service. They are the +same values any reader can obtain from the card without a key. Nothing else +about the card holder is sent. + +Transcripts +----------- + +With ``--apdu-log`` or ``--verbose``, relayed commands are written with their +data masked unless the instruction is a known-harmless read, because the +service chose the command and |cli| cannot vouch for what it carries. +``remote --full-transcript`` lifts that extra masking for troubleshooting; the +standard masking of secret-bearing commands still applies. diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index ad913e3..a8b7d5f 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -117,6 +117,34 @@ throwaway dev root: ``genuine verify --anchors ``. chain. It does **not** verify the per-card ISD or PIV-SSD keys (KDF/HSM-derived) — this tool has no access to those secrets, and the output says so. +Remote service +-------------- + +``remote`` commands exit with a code that names the failure class (see +:doc:`/exit-codes`): + +- **13** ``remote_connect``: the service could not be reached or the TLS + handshake failed. Check the network; the endpoint is fixed to + ``wss://piv.cryptnox.com``. With ``CRYPTNOX_REMOTE_URL`` set, only + ``authenticate`` and ``inspect`` run, and only to ``wss://`` or loopback. +- **14** ``remote_protocol``: the service sent a frame the tool cannot + interpret. Update the tool; if the newest version fails the same way, + report it with the message. +- **15** ``remote_policy``: the service asked for a card command the relay + policy refuses; the message names the command header. Nothing was sent to + the card for that command and the operation stopped. This is the tool + protecting the card; report the header so the policy can be reviewed. +- **16** ``remote_failed``: the exchange completed and the service reported + the operation as failed; the error object carries the service's result. + A per-card rate limit exists; wait before retrying. + +``remote reset`` and ``remote dev-reset`` refuse to run non-interactively +without ``--i-understand-this-is-irreversible``, and ``--yes`` is not +accepted for them. ``remote attest`` refuses non-interactively without +``--yes`` when the slot or the attestation container already holds a +certificate. + + yubico-piv-tool interop ------------------------- From 70c259a295fff2165fe8db69b93af2a58618748f Mon Sep 17 00:00:00 2001 From: Mladen Milankovic Date: Mon, 12 Oct 2026 16:43:51 +0200 Subject: [PATCH 2/3] State two relay policy limits in the remote guide The certificate write during an attestation is not bound to the slot, and a DELETE the policy cannot read is refused unless the channel encrypts commands. Co-Authored-By: Claude Fable 5.1 --- docs/remote/remote-guide.rst | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/remote/remote-guide.rst b/docs/remote/remote-guide.rst index ffacde7..e8d7eb1 100644 --- a/docs/remote/remote-guide.rst +++ b/docs/remote/remote-guide.rst @@ -58,8 +58,11 @@ Operations slot key), key generation for the requested slot only, and the certificate write. The tool then verifies the certificate against the pinned anchors and binds it to the card, the slot and the certificate stored on the card. The - generation itself happening on the card is the service's claim; nothing the - relay sees authenticates it. + certificate write itself is not bound to the slot: PUT DATA names its object + inside the data, so the service could write any PIV data object during the + operation, and only the certificate read back is checked. The generation + itself happening on the card is the service's claim; nothing the relay sees + authenticates it. ``reset`` Wipes and reinstalls the PIV function. The service authenticates to the @@ -72,12 +75,12 @@ Operations The relay policy for this operation admits card content management through the card manager only, lets a DELETE name nothing but the PIV instance, its - package and its security domain where the command travels in the clear, - caps authentication attempts per security domain, and stops at the first - failed authentication so no further attempt can spend a card-management - retry. Inside an encrypted channel the policy sees instructions, not - targets; that limit is real and is why the FIDO2 and genuineness state is - read before and after. + package and its security domain, refuses a DELETE it cannot read unless the + channel encrypts commands, caps authentication attempts per security + domain, and stops at the first failed authentication so no further attempt + can spend a card-management retry. Inside an encrypted channel the policy + sees instructions, not targets; that limit is real and is why the FIDO2 and + genuineness state is read before and after. ``dev-reset`` As ``reset``, for development cards, leaving the card manager and the PIV From 8d071375fa8047e160d43c9c123c8ed6e7d44ebe Mon Sep 17 00:00:00 2001 From: Mladen Milankovic Date: Mon, 12 Oct 2026 17:09:09 +0200 Subject: [PATCH 3/3] State what a remote inspect run costs the card The service enumerates key versions with one INITIALIZE UPDATE each and never completes an authentication, and the card counts every INITIALIZE UPDATE not followed by a successful EXTERNAL AUTHENTICATE: five in a row delay every INITIALIZE UPDATE by about 24 seconds until the next successful authentication. Say so in the inspect entries, state the rule once in the guide, and describe the --default-keys and reset failure cases as counted failed authentications rather than spent retries, since there is no lockout. Co-Authored-By: Claude Fable 5.1 --- docs/remote/remote-commands.rst | 10 ++++++---- docs/remote/remote-guide.rst | 25 ++++++++++++++++++++----- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/docs/remote/remote-commands.rst b/docs/remote/remote-commands.rst index ebd8229..160b919 100644 --- a/docs/remote/remote-commands.rst +++ b/docs/remote/remote-commands.rst @@ -35,8 +35,10 @@ Identification and inspection remote inspect probe the PIV function through the service: whether the PIV applet and its security domain are present - and which key versions they carry; key-free, the - relay refuses any authentication attempt + and which key versions they carry; key-free but not + free: each key version enumerated costs one + INITIALIZE UPDATE the card counts as a failed + authentication (see the guide) Both commands report the card as read locally (reader, ATR, CPLC UID), the service's result as the service asserted it, and a cross-check of the fields @@ -99,8 +101,8 @@ Reset --default-keys development cards only: the service authenticates with the publicly known GlobalPlatform default key instead of deriving this card's own. A production - card refuses it, and the failed authentication costs - a card-management retry. + card refuses it, and the card counts one failed + authentication. Before asking for consent, both commands read and show what the card holds: the PIV personalization state and data objects, the PIV security domain and diff --git a/docs/remote/remote-guide.rst b/docs/remote/remote-guide.rst index e8d7eb1..152a08d 100644 --- a/docs/remote/remote-guide.rst +++ b/docs/remote/remote-guide.rst @@ -47,8 +47,12 @@ Operations ``inspect`` Probes whether the PIV applet and its security domain are present and which - key versions they carry. Key-free: an authentication attempt from the - service is refused before it reaches the card. + key versions they carry. Key-free: the relay refuses every EXTERNAL + AUTHENTICATE, so the service never completes an authentication. Not free: + the service enumerates key versions with one INITIALIZE UPDATE each, two on + a card as shipped, and the card counts each of them as a failed + authentication (see below). Two or three runs in a row reach the delay. The + relay admits at most four INITIALIZE UPDATEs per run. ``attest`` Generates a key in one slot and returns a key-attestation certificate for @@ -78,7 +82,7 @@ Operations package and its security domain, refuses a DELETE it cannot read unless the channel encrypts commands, caps authentication attempts per security domain, and stops at the first failed authentication so no further attempt - can spend a card-management retry. Inside an encrypted channel the policy + adds to the card's count. Inside an encrypted channel the policy sees instructions, not targets; that limit is real and is why the FIDO2 and genuineness state is read before and after. @@ -90,6 +94,17 @@ Operations genuine until it is reset through the service again. The development access credential comes from ``CRYPTNOX_REMOTE_DEV_TOKEN`` only. +What a failed authentication costs +---------------------------------- + +The card counts every INITIALIZE UPDATE that is not followed by a successful +EXTERNAL AUTHENTICATE, whichever security domain it was sent to. After five in +a row, every INITIALIZE UPDATE on the card, in any security domain, is delayed +by about 24 seconds until the next successful authentication clears the count. +There is no lockout, only the delay. This is why the relay caps authentication +attempts per operation. ``piv admin authenticate`` with the card's own keys is +one way to clear the count. + The ``--default-keys`` flag --------------------------- @@ -98,8 +113,8 @@ key, derived per card from its identity. That is the state a card leaves the factory in. A development card, or one deliberately put on the published GlobalPlatform default key, needs ``--default-keys`` instead; without it the service presents a derived key the card does not hold, the authentication -fails, and one of the card's bounded card-management retries is spent. The -flag is the caller's statement about the card, so the default is the safe one: +fails, and the card counts one failed authentication (see above). The flag is +the caller's statement about the card, so the default is the safe one: forgetting it on a development card costs a failed authentication and nothing else, while the reverse mistake on a production card would present a published key to a card that should only ever answer to its own.