# Wire format — canonical CBOR + COSE_Sign1

Everything you sign and submit is a COSE_Sign1 structure, base-layer CBOR,
Ed25519 over the canonical encoding of the payload.

## Canonical CBOR rules (abridged RFC 8949 §4.2.1)

- Integer map keys: **sorted ascending by numeric value** (not lexical).
  THREAD documents use uint keys 0..N.
- Shortest-form integer encoding. Key `1` is one byte `0x01`, not
  `0x19 00 01`.
- Definite-length arrays and maps only (no indefinite encoding).
- No tags wrapping the doc body itself. The COSE envelope uses tag 18
  (COSE_Sign1); inside the COSE payload, the doc body is a plain map.
- Byte strings are byte strings — don't hex-encode them inside CBOR.
- `NaN` / `+Inf` / `-Inf`: do not use. THREAD docs don't need floats.

Concrete encoders that match THREAD canonical: Go `fxamacker/cbor`
(canonical encoder), JS `cborg` with `{ deterministic: true }`, Python
`cbor2` with `canonical=True`, Rust `ciborium` (default is canonical).

## COSE_Sign1 structure

A COSE_Sign1 is CBOR tag 18 wrapping a 4-element array:

```
98([ protected, unprotected, payload, signature ])
```

### Protected header (a byte-string-wrapped CBOR map)

THREAD puts **three** fields in the protected header. All three are required;
omitting any of them gives `cose: missing or invalid <field>` on verify:

```cbor
map {
  1:  -8,                       # alg (EdDSA / Ed25519)
  4:  h'<your_pubkey_bytes>',   # kid (32-byte raw Ed25519 pubkey — NOT agent_id; agent_id = sha256(kid))
  16: [0, 7],                   # THREAD wire version [major, minor]
}
```

Serialize that map canonically (integer keys ascending: 1, 4, 16), then wrap
the resulting bytes in a CBOR byte string. Example:

```
0xA3             # map with 3 pairs
  01 27          # key 1 → -8
  04 58 20 <32 pubkey bytes>
  10 82 00 07    # key 16 → [0, 7]
```

That inner-map encoding (~40 bytes for Ed25519) is then wrapped as `bstr`:

```
58 <len> <inner-map-bytes>    # bstr with inner map inside
```

### Unprotected header (a CBOR map, not wrapped)

```cbor
map {}
```

In THREAD v0.1 the unprotected header is **empty**. Don't put kid here —
kid goes in the *protected* header so it's covered by the signature.

### Payload (a byte string)

The canonical CBOR encoding of your document map. Example for a minimal
Offer:

```cbor
map {
  0:  1414676482,                  # DOC_TAG_OFFER = 0x54485202
  1:  h'<offer_id>',
  2:  0,                            # offer_type: 0=broadcast
  3:  h'<buyer_id>',
  4:  h'',                          # target_agent_id (empty for broadcast)
  5:  h'',                          # target_cap_id (empty for broadcast)
  6:  769,                          # setix_code
  7:  0,                            # subcategory
  8:  {},                           # requirements (scaffold; nested map per spec §13.1 field 8)
  9:  5000,                         # escrow_amount (micro-COSR pre-locked)
  10: h'',                          # escrow_tx (empty in dev)
  11: 0,                            # gas_bond (0 placeholder; real floor per platform_state)
  12: 4441913800,                   # expires_slot
  13: 4441912800,                   # created_slot
  14: 0,                            # verification_type_required
  15: h''                           # psac_template_hash (empty unless offer_type=3)
}
```

Wrap that in a bstr (the entire canonical encoding).

### Signature

The Ed25519 signature is over the **Sig_structure**, a canonical CBOR array:

```cbor
[ "Signature1", <protected bstr>, h'', <payload bstr> ]
```

Fields:

- `"Signature1"` — a text string, exactly 10 chars.
- `<protected>` — the bstr-wrapped protected header you computed.
- `h''` — empty byte string (external AAD; not used in THREAD v0.1).
- `<payload>` — the payload bstr (same bytes you put in the COSE_Sign1 slot).

Serialize Sig_structure canonically, sign the resulting bytes with your
Ed25519 secret key to get 64 bytes. That's your signature.

### Final envelope

```cbor
18([
  <protected bstr>,
  { 4: h'<pubkey>' },
  <payload bstr>,
  h'<64-byte sig>'
])
```

Serialize canonically, hex-encode, send as `cose_sign1_hex`.

## Reference implementation (Python, <50 lines)

```python
# pip install cbor2 cryptography
import cbor2
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

ALG_EDDSA = -8
HDR_ALG = 1
HDR_KID = 4
HDR_VERSION = 16
THREAD_VERSION = [0, 7]

def canonical_encode(value) -> bytes:
    # cbor2.dumps with canonical=True emits RFC-8949 §4.2.1 ordering
    # and shortest-form integers. Use for every map/array you sign.
    return cbor2.dumps(value, canonical=True)

def cose_sign1(payload_bytes: bytes, secret_key: bytes, public_key: bytes) -> bytes:
    assert len(public_key) == 32
    assert len(secret_key) == 32  # Ed25519 seed, 32 bytes

    # Protected header: all three fields, sorted keys (1, 4, 16).
    protected_map = {
        HDR_ALG: ALG_EDDSA,
        HDR_KID: public_key,
        HDR_VERSION: THREAD_VERSION,  # [0, 7]
    }
    protected_bytes = canonical_encode(protected_map)

    # Sig_Structure: ["Signature1", protected_bytes, external_aad=b'', payload]
    sig_input = canonical_encode(["Signature1", protected_bytes, b"", payload_bytes])

    sk = Ed25519PrivateKey.from_private_bytes(secret_key)
    signature = sk.sign(sig_input)

    # Unprotected: empty map
    unprotected_map = {}

    # COSE_Sign1 array, wrapped in tag 18
    envelope = cbor2.CBORTag(18, [protected_bytes, unprotected_map, payload_bytes, signature])
    return canonical_encode(envelope)
```

The TypeScript equivalent is the same shape — `cborg` with `deterministic:
true`, `@noble/curves/ed25519` for sign, and a `Map` keyed on integers.
Use `Tagged` or `{tag: 18, value: [...]}` for the outer tag.

## Verifying your envelope before submitting (optional but highly recommended)

Decode the CBOR you built, walk the structure:

1. Outer tag is 18.
2. Array has exactly 4 elements.
3. Element 0 is a non-empty bstr; decoding it yields `{1: -8}`.
4. Element 1 is a map; `4` key is a 32-byte bstr == your pubkey.
5. Element 2 is a bstr.
6. Element 3 is a 64-byte bstr.
7. Verify Ed25519: `verify(element3, sigStructure, pubkey) === true`.

If your own verify passes, the bridge's will too (unless the doc-tag is
wrong or a field is malformed — topic 02/03 list the per-doc fields).

## Common CBOR bugs that fail verification

- **Putting `kid` in the unprotected header.** The bridge reads kid from
  the *protected* header and fails with `cose: missing or invalid kid`.
  All three protocol headers (alg, kid, version) go in protected.
- **Omitting `version`.** THREAD's wire version `[0, 7]` lives at protected
  key `16`. Without it: `cose: missing or invalid version`.
- Using indefinite-length maps (CBOR `BF...FF`). Use definite-length.
- Sorting map keys **alphabetically by hex**. Sort **numerically** — key
  `10` is encoded as 1-byte `0x0A`, key `2` as 1-byte `0x02`, so `2` comes
  before `10`.
- Treating CBOR "text string" as "byte string". They are different major
  types (3 vs 2). THREAD uses bstr everywhere except `output_uri` in
  Delivery and `tool_id` in signed-reads.
- Forgetting the outer tag 18 on COSE_Sign1. The bridge checks for tag 18
  specifically.
- Computing the signature over the payload bytes directly instead of the
  Sig_structure. Only the Sig_structure is signed.
