Skip to content

fix(ctap2): store credential public keys as opaque COSE bytes#229

Merged
AlfioEmanueleFresta merged 2 commits into
masterfrom
feat/cose-opaque-credential-key
May 28, 2026
Merged

fix(ctap2): store credential public keys as opaque COSE bytes#229
AlfioEmanueleFresta merged 2 commits into
masterfrom
feat/cose-opaque-credential-key

Conversation

@AlfioEmanueleFresta
Copy link
Copy Markdown
Member

The credential public key returned by an authenticator was being stored in a closed Rust enum that only handled P-256 and Ed25519. Any credential using RSA, P-384, secp256k1 or a post-quantum algorithm failed to deserialise, so the whole registration response was rejected.

Credential public keys are now stored as raw COSE bytes. The platform doesn't crypto-validate them, it forwards them to the relying party, so the closed enum was over-modelling. The authenticator data parser captures the COSE_Key bytes verbatim, preserving the attestation signature's bit-identity for relying-party verification.

A small helper reads the mandatory alg parameter per WebAuthn L3 §6.5.2, which is what populates publicKeyAlgorithm in the JSON response. The previous ES256 fallback when attested credential data was missing is removed because the AT flag MUST be set per CTAP §6.1, and a silent fallback was hiding real protocol violations.

The closed enum is still used for the ECDH-ES P-256 key agreement during PIN/UV protocols, where the COSE shape is fixed by spec.

Stacked on top of #228.

Copy link
Copy Markdown
Collaborator

@msirringhaus msirringhaus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm. One comment update would be nice, though.

) -> Result<Self::IdlModel, ResponseSerializationError> {
// Get credential ID from attested credential data
let credential_id_bytes = self
// The AT flag MUST be set on makeCredential responses per CTAP §6.1.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either provide a link or specify which CTAP-version is meant here, because §6 is something different for each release. A direct quote would probably also be nice, because I couldn't find it right away.

Base automatically changed from feat/cose-alg-passthrough to master May 28, 2026 17:37
cosey::PublicKey is a closed enum of P-256, Ed25519 and ECDH-ES+HKDF-256.
Any credential using RSA, P-384, P-521, secp256k1 or a post-quantum
algorithm cannot be deserialised at all. The platform does not
crypto-validate credential public keys, so it now stores them as raw
COSE_Key bytes and forwards them to the relying party verbatim.

The authData deserialiser captures the COSE_Key span through the CBOR
cursor so the attestation signature over those bytes stays valid for
RP verification.

A new internal cose::read_alg helper reads the mandatory `alg`
parameter per WebAuthn L3 §6.5.2, which is how the make_credential
response now populates getPublicKeyAlgorithm(). The previous ES256
fallback for missing attested credential data is removed, since the
AT flag MUST be set per CTAP §6.1.

cosey stays in use for the ECDH-ES P-256 key agreement during PIN/UV
protocols, where the COSE shape is fixed by spec.
@AlfioEmanueleFresta AlfioEmanueleFresta force-pushed the feat/cose-opaque-credential-key branch from 0ac08f3 to 1c17ed4 Compare May 28, 2026 17:53
@AlfioEmanueleFresta AlfioEmanueleFresta merged commit 9e48445 into master May 28, 2026
4 checks passed
@AlfioEmanueleFresta AlfioEmanueleFresta deleted the feat/cose-opaque-credential-key branch May 28, 2026 18:49
AlfioEmanueleFresta added a commit that referenced this pull request May 28, 2026
WebAuthn L3 §5.2.1.1 says
`AuthenticatorAttestationResponse.getPublicKey()` returns DER-encoded
SubjectPublicKeyInfo for credentials using ES256, EdDSA Ed25519 and
RS256, and null for any algorithm the user agent does not implement.
libwebauthn was emitting raw COSE bytes there instead, which relying
parties could not feed into `SubtleCrypto` or standard X.509 parsers.

A new converter produces SPKI for the WebAuthn L3 floor plus ESP256,
which is equivalent to ES256 per RFC 9864. Algorithms outside the
supported set return null per spec. Malformed keys for understood
algorithms surface as an error.

The conversion uses the `spki` and `der` crates from RustCrypto, both
already in the transitive dependency tree via `p256`, so no new external
runtime crates land. Adding new algorithm support later is a small
per-algorithm addition.

Stacked on top of #229.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants