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.
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.
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.
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
- Public Key vs Symmetric Credential Types — the parent topic area and the cryptographic model
- Cryptographic Algorithms Supported by WebAuthn — choosing which algorithms to offer
- How to Store WebAuthn Public Keys in PostgreSQL — the column types this decoding implies
- Handling WebAuthn Signature Verification in Node.js — using the assembled key