Decoding COSE Key Structures

The credential public key arrives as a COSE key: a CBOR map with integer labels whose meaning depends on the key type. This page covers the labels you will actually meet, the dispatch that has to happen before any of them can be read, and how to turn the result into something a verifier will accept. It sits under Public Key vs Symmetric Credential Types, and the key itself is reached by the walk described in decoding authenticatorData.


Structure Reference

Label EC key RSA key Notes
1 kty = 2 kty = 3 read this first, always
3 alg alg matches what you offered in the options
-1 curve identifier modulus same label, different meaning
-2 x coordinate public exponent fixed width for EC, variable for RSA
-3 y coordinate absent for RSA

The reuse of negative labels across key types is the single most important thing to internalise. There is no way to interpret -1 without first knowing kty.

The COSE map keys you will meet A COSE key is a CBOR map with integer keys; five of them cover every WebAuthn credential in practice. Integer keys, and their meaning depends on the key type 1 — kty key type: 2 for elliptic curve, 3 for RSA, 1 for the Edwards family 3 — alg the negotiated algorithm identifier, stored alongside the key -1 the curve for EC keys, or the RSA modulus depending on kty -2 / -3 the x and y coordinates for EC keys -1 / -2 (RSA) modulus and exponent — note the key numbers are reused across types

Root Cause Analysis

1. Assuming elliptic curve. The overwhelming majority of credentials are ES256, so a decoder written against that case works for months and then meets a Windows Hello configuration or an enterprise authenticator producing RSA. It reads the modulus as an x coordinate, assembles a key that is structurally valid and cryptographically meaningless, and verification fails with no useful error.

2. Dropping the algorithm. The alg label tells you which verification path to take later. Storing the key without it forces the verifier to guess, and guessing wrong is indistinguishable from a bad signature.

3. Re-encoding the map. CBOR permits several encodings of the same logical map. A decoder that parses and re-serialises will not necessarily reproduce the original bytes, and if anything downstream hashes or compares them the mismatch is silent.

4. Trimming leading zeros. EC coordinates are fixed-width for a given curve, and a leading zero byte is part of the value. Stripping it — which some conversion helpers do — produces a coordinate one byte short and a key that fails.

From CBOR map to a verifiable key The map is decoded, dispatched on key type, and converted into whatever structure your verifier accepts. Four steps, and the second one decides the other two CBOR map as stored, verbatim kty dispatch on key type assemble SPKI or JWK fields import a verifier-ready key Store the original CBOR bytes as well: a future verifier may want different input, and the bytes are the only authoritative source.

Step-by-Step Resolution

Step 1 — decode with a strict CBOR reader

Use a decoder that rejects trailing data and reports how many bytes it consumed. The second property is what lets you find extension output when it follows the key.

Step 2 — read kty before anything else

Branch on it immediately. Everything after this point depends on which branch you are in.

Step 3 — read alg and keep it

Store it alongside the key material in its own column. It is small, it never changes, and it is what makes verification deterministic.

Step 4 — assemble for your verifier

For elliptic curve keys, most stacks want either an SPKI structure or a JWK with the curve name and the two coordinates. For RSA, a modulus and exponent. Build whichever your verifier accepts, at the point of use rather than at storage time.

Step 5 — store the original bytes

Whatever representation you assemble, persist the COSE bytes exactly as received. They are the authoritative source, and a future change of verification library will want them.

Why the negative keys are confusing COSE reuses negative labels across key types, so the same number means different things depending on kty. Read kty before reading anything else for an EC key -1 is the curve identifier -2 is the x coordinate -3 is the y coordinate all fixed width for a given curve for an RSA key -1 is the modulus -2 is the public exponent there is no -3 lengths vary with key size A decoder that assumes elliptic curve will read an RSA modulus as an x coordinate and produce a key that fails silently.

Verification and Testing

Keep one real registration fixture per algorithm you accept. Synthetic keys generated by your own code test the code against itself; a captured key from an actual authenticator tests it against reality, which is where the leading-zero and label-reuse bugs live.

Assert a full round trip: decode the stored COSE bytes, assemble a verifier key, and verify a captured assertion that is known to be valid. Anything less proves only that the decoder does not throw.

Add a negative test with an RSA key run through the elliptic-curve path. It should fail loudly at the dispatch rather than quietly at verification, which is the difference between a five-minute diagnosis and an afternoon.


Pitfalls

1. One verification helper for all algorithms. It always contains a hidden assumption; make the dispatch explicit.

2. Storing the assembled form only. You lose the ability to re-derive a different representation later.

3. Treating the map as ordered. CBOR maps have no meaningful order; read by label, never by position.


Frequently Asked Questions

Which algorithms will I actually receive?

ES256 for the overwhelming majority of credentials, RS256 from some Windows Hello configurations and enterprise authenticators, and EdDSA occasionally. If you offered all three in your registration options, you must be able to decode and verify all three.

Is the COSE key the same thing as a JWK?

They describe the same information with different encodings and different label conventions. Many libraries convert between them, and a JWK is often the most convenient intermediate representation because web crypto implementations accept it directly. The conversion is where label-reuse mistakes surface, so test it against a real key of each type.

Should the key be stored as binary or as text?

Binary, in a column typed for bytes. Encoding it as base64 in a text column adds a conversion at both ends and a place for a padding difference to change the bytes without anybody noticing.

Does the key ever change for a credential?

No. A credential’s key pair is created once and never rotates; a new key means a new credential. Any code path that updates a public key on an existing credential row is a bug.

What if alg in the key disagrees with what I offered?

Trust the key. The authenticator chose from your list and reported its choice, and that choice is what the signature will be produced with. A disagreement usually means your options were assembled from a different configuration than you thought — worth investigating, but the stored value must reflect what came back.

How large are these keys?

An elliptic-curve key is small — well under a hundred bytes. An RSA key is an order of magnitude larger, which is worth knowing when sizing a column and when an allow-list carrying many credentials starts approaching a message-size limit on a constrained transport.

Can I derive the credential id from the key?

No. They are independent values: the credential id is chosen by the authenticator and may encode wrapped key material for a server-side credential, while the public key is the verification material. Treat them as unrelated columns that happen to arrive together.


What does a malformed key usually indicate?

An encoding boundary rather than a hostile client. In practice the sequence is that the bytes arrived correctly, were stored through a path that treated them as text, and came back subtly different — a base64 alphabet mismatch, a collation that normalised something, a JSON serialiser that turned a byte array into a string of numbers. The diagnostic is always the same: hash the bytes at ingress and hash them again immediately before decoding, and compare. If the digests differ, the fault is in the storage path and no amount of examining the decoder will find it.

That single comparison has settled more of these investigations than any amount of examining the decoder itself.

Related