---
title: "COSE and WebAuthn"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{COSE and WebAuthn}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
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.

```{r}
library(zucbor)
hex <- function(h) as.raw(strtoi(substring(h, seq(1, nchar(h), 2), seq(2, nchar(h), 2)), 16L))
```

## A WebAuthn attestation object

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":

```{r}
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.

```{r}
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:

```{r}
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:

```{r}
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.

## Re-encoding what was signed

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:

```{r}
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 signed CWT

A CBOR Web Token is a COSE_Sign1 message, tag 18, whose payload is a map of
claims. This is RFC 8392's example:

```{r}
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:

```{r}
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:

```{r}
to_be_signed <- cbor_encode(list("Signature1", msg$value[[1]], raw(), msg$value[[3]]))
cbor_diagnose(to_be_signed)
```
