knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
COSE (RFC 9052), CWT (RFC 8392) and WebAuthn all carry their data as CBOR. This vignette takes structures from their specifications apart with zucbor. It stops where cryptography starts: zucbor gives you the exact bytes to verify, and another package verifies them.
library(zucbor) hex <- function(h) as.raw(strtoi(substring(h, seq(1, nchar(h), 2), seq(2, nchar(h), 2)), 16L))
When a passkey is created, the authenticator returns an attestation object. This one is the WebAuthn Level 3 test vector "ES256 Credential with No Attestation":
att <- hex(paste0( "a363666d74646e6f6e656761747453746d74a068617574684461746158a4bfabc374", "32958b063360d3ad6461c9c4735ae7f8edd46592a5e0f01452b2e4b5590000000084", "46ccb9ab1db374750b2367ff6f3a1f0020f91f391db4c9b2fde0ea70189cba3fb63f", "579ba6122b33ad94ff3ec330084be4a5010203262001215820afefa16f97ca9b2d23", "eb86ccb64098d20db90856062eb249c33a9b672f26df61225820930a56b87a2fca66", "334b03458abf879717c12cc68ed73290af2e2664796b9220")) ao <- cbor_decode(att) str(ao)
authData is a byte string with its own binary layout: a 32-byte hash of
the relying party's ID, a flags byte, a 4-byte counter, a 16-byte AAGUID,
a 2-byte credential ID length and the ID, then the credential's public key
as a COSE_Key, which is CBOR again.
ad <- ao$authData id_len <- as.integer(ad[54]) * 256L + as.integer(ad[55]) rest <- ad[-seq_len(55 + id_len)] key <- cbor_decode_prefix(rest) key$value
cbor_decode_prefix() decodes the one item at the start of rest and says
how many bytes it took, without reading past it. That matters here: the
key's length is not written anywhere, and what follows it, if anything, is
up to the flags byte, not up to CBOR. A COSE_Key uses integer map keys, so
it is a cbor_map. Key 1 is the key type (2, EC2), 3 the algorithm (-7,
ES256), -1 the curve (1, P-256), and -2 and -3 the coordinates.
cbor_diagnose() shows the same thing the way the specifications write it:
cbor_diagnose(rest[seq_len(key$consumed)])
Bit 7 of the flags byte (ED) says whether extensions follow the key, as
one more CBOR map. Here it is clear, and nothing follows:
flags <- as.integer(ad[33]) bitwAnd(flags, 0x80L) != 0L # extensions? length(rest) - key$consumed # bytes after the key
Had it been set, cbor_decode(rest[-seq_len(key$consumed)]) would read
them, and would refuse anything after the extensions, as the layout
requires.
Signature checks run over exact bytes. zucbor's encoder is deterministic, and decoding then re-encoding deterministic input reproduces it exactly, so a structure can be taken apart, inspected and put back:
identical(cbor_encode(ao), att)
That holds because one-element arrays decode as I() values and re-encode
as arrays, booleans never turn into numbers, and whole numbers stay
integers. Across all 651 messages and signed structures in the COSE working
group's examples, every one in deterministic form survives the round trip
byte for byte.
A CBOR Web Token is a COSE_Sign1 message, tag 18, whose payload is a map of claims. This is RFC 8392's example:
cwt <- hex(paste0( "d28443a10126a104524173796d6d657472696345434453413235365850a70175636f", "61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a", "2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a56", "10d9f007420b7158405427c1ff28d23fbad1f29c4c7c6a555e601d6fa29f9179bc3d", "7438bacaca5acd08c8d4d4f96131680c429a01f85951ecee743a52b9b63632c57209", "120e1c9e30")) msg <- cbor_decode(cwt) msg$tag str(msg$value, max.level = 1)
The protected header and the payload are byte strings holding more CBOR. Decoding them is explicit, so each gets the same checks and limits:
cbor_decode(msg$value[[1]]) # protected header: {1: -7}, ES256 claims <- cbor_decode(msg$value[[3]]) claims
To verify the signature, a verifier builds the Sig_structure of RFC 9052
section 4.4 and checks the signature over its bytes. With zucbor that is
one call, and the bytes are exactly the ones the signer produced, because
the encoding is deterministic:
to_be_signed <- cbor_encode(list("Signature1", msg$value[[1]], raw(), msg$value[[3]])) cbor_diagnose(to_be_signed)
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.