Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Enterprise attestation (org provisioning)

Out of the box ordinary makeCredential stays packed self-attestation with no certificate at all. RS-Key does carry a per-device self-signed certificate (a P-256 X.509 leaf with CN RSK FIDO2, built over the seed at first boot), but it presents that only on U2F registration and EA-level-2 requests. An organization can replace that device cert with its own attestation key and certificate chain, so its relying parties can verify “this credential was created on one of our keys” — the CTAP 2.1 enterprise-attestation (EA) feature.

This page is for the team that provisions fleet keys. If you do not work for an org that has provisioned yours, it is a no-op: nothing here changes how an unprovisioned key behaves, and EA is never served unless a managed platform explicitly asks for it.

The key + chain model

Two pieces of state make up an org attestation, stored separately on the device:

Stored asHoldsSealing
EF_ATT_KEY (0xCE10)the org attestation P-256 private scalarkbase-sealed, exactly like the master seed
EF_ATT_CHAIN (0xCE11)the DER certificate chain, leaf first (count ‖ (len ‖ der)*)public material, stored plain

The key signs each attestation; the chain is what relying parties walk back to your CA. The leaf’s public key must match the imported scalar — the device does not check this (framing only), so a key/chain mismatch surfaces as your own relying party’s first signature-verification failure, not an import error.

Provisioning

Generate an attestation CA and a leaf however your PKI does it. The leaf’s subject public key must be the P-256 point of the private key you import; only P-256 (secp256r1) keys are accepted — rsk rejects any other curve before it touches the device.

# host-side, with your PKI:
#   org-att.pem    P-256 private key (PEM)
#   org-chain.pem  leaf cert first, then intermediates, then (optionally) the CA
rsk fido attestation import --key org-att.pem --chain org-chain.pem [--pin …]
rsk fido attestation status

--chain takes a PEM bundle (concatenated -----BEGIN CERTIFICATE----- blocks) or already-concatenated DER. Limits, enforced host-side and again in firmware:

LimitValue
CurveP-256 only
Chain size≤ 2048 bytes total
Certs in chain≤ 4

status is ungated and prints whether a chain is installed plus the SHA-256 of the packed chain (so you can confirm a fleet is on the right CA without moving any secret):

$ rsk fido attestation status
org attestation : installed
chain hash      : 9f2c…

To roll back to the factory self-signed cert:

rsk fido attestation clear [--pin …]

What changes once a chain is installed

  • makeCredential with enterpriseAttestation 1 or 2 (sent by managed platforms) returns a full attestation: signature by the org key, x5c = your chain (leaf first), and the ep response flag (true). With an org key installed, both EA levels emit the org attestation.
  • U2F / CTAP1 registration attests with the chain’s leaf instead of the self-signed device cert (classic batch attestation — a U2F response carries exactly one certificate, so only the leaf travels).
  • Ordinary makeCredential is untouched — packed self-attestation, no chain, no cross-site trackable identifier. EA fires only when the platform sets the enterpriseAttestation request field and enableEnterpriseAttestation is on (below).

Without an org chain (the default)

If no org key is provisioned, the request field still has an effect, per the spec:

EA levelWithout org keyWith org key
(absent / 0)self-attestationself-attestation
1 — vendor-facilitatedself-attestationfull org attestation
2 — platform-managedfull attestation by the device key + self-signed RSK FIDO2 certfull org attestation

So a stock key already answers an EA-level-2 request with a real “basic” attestation — just under its own per-device cert rather than a shared chain. The device key and that self-signed cert are the same pair U2F register uses.

Enabling EA on the device (enableEnterpriseAttestation)

Importing the key is not enough. A makeCredential with the EA field is honored only after enableEnterpriseAttestation (CTAP 2.1 authenticatorConfig, subcommand 0x01) has been issued. RS-Key has no rsk command for this — it is the managed platform’s job (the OS/MDM/browser stack that drives EA), and it requires an acfg pinUvAuthToken, i.e. a FIDO PIN must be set. getInfo reports the current state in the ep option, which the firmware mirrors straight from EF_EA_ENABLED:

# python-fido2, the same library `rsk` uses:
python3 - <<'PY'
from fido2.hid import CtapHidDevice
from fido2.ctap2 import Ctap2
info = Ctap2(next(CtapHidDevice.list_devices())).info
print("ep =", info.options.get("ep"))   # True once enableEnterpriseAttestation ran
PY

enableEnterpriseAttestation persists across power cycles — it is written to flash (EF_EA_ENABLED), as CTAP 2.1 specifies. It is cleared only by authenticatorReset (see below).

Transport and gating

The P-256 private scalar crosses USB ChaCha20-Poly1305-wrapped on the same ephemeral-ECDH channel (MSE handshake: P-256 ECDH → HKDF-SHA256 → ChaCha20-Poly1305) the seed backup uses. The chain is public certificate material and travels in the clear, MAC-covered by the PIN token like every subcommand parameter.

Import (0x09) and clear (0x0A) are gated exactly like a seed move: channel + PIN (when one is set) + physical touch — on this board the touch is the BOOTSEL button (build.md). status (0x0B) is ungated; the chain it returns is public. Both mutations land in the audit journal (ATT_IMPORT / ATT_CLEAR), and so does an enableEnterpriseAttestation (CFG_EA).

rsk fido attestation import …    # → "touch the device (BOOTSEL) to authorise…"
rsk fido attestation clear …     # → "touch the device (BOOTSEL) to remove…"

On the device the key is sealed under the same kbase arms as the master seed, and the seal tag records which arm wrapped it — so importing before or after the OTP burn both stay loadable. Burn the OTP master key before importing and the sealed attestation key is rooted in fuses, not just flash (otp-fuses.md).

Reset semantics

authenticatorReset wipes FIDO user state, but the org provisioning splits across that line:

StateSurvives authenticatorReset?
EF_ATT_KEY (org key)yes — org-provisioned device identity, not user data
EF_ATT_CHAIN (chain)yes
EF_EA_ENABLED (the enable flag)no — wiped with PIN, credentials, counter

So a factory reset leaves the org attestation installed but switches EA off: the managed platform must re-issue enableEnterpriseAttestation before EA fires again. The reset itself is recorded in the audit journal. Removing the key and chain is the explicit, gated attestation clear — nothing else clears them.

Privacy note

A shared org chain makes credentials linkable to the organization across its relying parties — that is the entire point of EA, and why the spec gates it behind both an explicit per-request field and a device-wide enable. Ordinary (non-EA) makeCredential stays self-attested and unlinkable; the org chain is served only on explicit EA requests.

Troubleshooting

  • attestation key must be P-256 (got …) — the --key PEM is the wrong curve. RS-Key attests with ECDSA P-256 only; re-issue the org key on secp256r1.
  • chain too large (… B, max 2048) — trim the bundle. You rarely need the root CA in x5c; leaf + one intermediate is usually enough, and the leaf alone is all U2F can carry.
  • device requires a PIN — pass --pin (status 0x36) — import/clear are gated; set a FIDO PIN first (rsk fido set-pin) and pass it.
  • An EA makeCredential comes back self-attested (no x5c, no ep) — either enableEnterpriseAttestation was never issued (check options.ep), or it was cleared by a factory reset; have the managed platform re-enable it.
  • import failed: 0x33PIN_AUTH_INVALID: the PIN was wrong, or its token lacked the acfg permission. Re-run with the correct --pin (do not guess — wrong attempts burn PIN retries).
  • The import hangs at “touch the device…” — the physical touch never arrived; press the BOOTSEL button while the prompt is up, then it completes.