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 as | Holds | Sealing |
|---|---|---|
EF_ATT_KEY (0xCE10) | the org attestation P-256 private scalar | kbase-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:
| Limit | Value |
|---|---|
| Curve | P-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
enterpriseAttestation1 or 2 (sent by managed platforms) returns a full attestation: signature by the org key,x5c= your chain (leaf first), and theepresponse 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
enterpriseAttestationrequest field andenableEnterpriseAttestationis 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 level | Without org key | With org key |
|---|---|---|
| (absent / 0) | self-attestation | self-attestation |
| 1 — vendor-facilitated | self-attestation | full org attestation |
| 2 — platform-managed | full attestation by the device key + self-signed RSK FIDO2 cert | full 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:
| State | Survives 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--keyPEM 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 inx5c; leaf + one intermediate is usually enough, and the leaf alone is all U2F can carry.device requires a PIN — pass --pin(status0x36) — import/clear are gated; set a FIDO PIN first (rsk fido set-pin) and pass it.- An EA
makeCredentialcomes back self-attested (nox5c, noep) — eitherenableEnterpriseAttestationwas never issued (checkoptions.ep), or it was cleared by a factory reset; have the managed platform re-enable it. import failed: 0x33—PIN_AUTH_INVALID: the PIN was wrong, or its token lacked theacfgpermission. 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.