Agent Action Capsule · explain
03 / 11

COSE and deterministic encoding

A signature over a digest checks only if the verifier hashes the same bytes the signer hashed, so a structured record must turn into exactly one byte string.

Same record, two encoders Stage 1: on the left a box holds one logical record, an object with the keys 😀 (U+1F600) and Z (U+FF3A); two lanes lead right, Implementation A on top and Implementation B below. Stage 2: Implementation A applies JCS, sorting keys by UTF-16 code units, and emits the bytes with 😀 ahead of Z, because its leading code unit D83D is lower than FF3A; Implementation B sorts by code point and emits the bytes with Z ahead of 😀. Stage 3: A hashes its bytes to capsule_id 9d07be6c through d2cf5dde, and its Producer Envelope signs those 32 bytes; B hashes its different bytes to a different digest; the comparison shows not equal, and B's check against A's envelope fails, marked with a red cross. Stage 4: B switches to the declared encoding, plain JCS; its bytes are now identical to A's, its digest is the same 9d07be6c through d2cf5dde, the comparison shows equal, and the envelope verifies for both, marked with a green check. A note records that the code-point sort in stages 2 and 3 gave different bytes and failed. one logical record 😀 → "hi" Z → "wide" Implementation A Implementation B JCS (RFC 8785) sort by UTF-16 code unit {"😀":"hi","Z":"wide"} D83D… < FF3A own rule sort by code point {"Z":"wide","😀":"hi"} U+FF3A < U+1F600 SHA-256 = capsule_id 9d07be6c…d2cf5dde Producer Envelope (COSE_Sign1) signs these 32 bytes SHA-256 a different digest ≠ ✗ fails JCS (RFC 8785) sort by UTF-16 code unit {"😀":"hi","Z":"wide"} identical bytes SHA-256 = capsule_id 9d07be6c…d2cf5dde = ✓ verifies earlier: a code-point sort gave different bytes and failed

1What it is

A COSE_Sign1 (RFC 9052) is a CBOR structure holding a protected header, an unprotected header, a payload, and one signature computed over the encoded protected header and the payload.

A signature over a digest can be checked only by hashing exactly the bytes the signer hashed, so two implementations that rebuild those bytes from the same structured record must encode it the same way.

JSON records here use the JSON Canonicalization Scheme (JCS, RFC 8785), and for CBOR content the COSE deterministic encodings Internet-Draft in preparation lets a COSE Hash Envelope name the deterministic encoding that was applied.

2Design choices

Chosen: the Producer Envelope's payload is the raw 32 bytes of capsule_id; the protected header holds exactly alg -8 (Ed25519), content type application/agent-action-capsule-id, and kid (the raw 32-byte public key); the unprotected header is empty.
Rejected: signing hex text or the Capsule JSON. The envelope signs the stable identity, so the Capsule needs no re-serialization at verify time. (draft-mih-scitt-agent-action-capsule-05 §3.1)
Chosen: registration in a transparency log uses a separate SCITT Signed Statement over the same raw capsule_id, with CWT iss and sub in the protected header.
Rejected: treating the bare envelope as a SCITT Signed Statement, which RFC 9943 requires to carry CWT iss and sub; the three-entry envelope header has neither. (draft-mih-scitt-agent-action-capsule-05 §3.3)
Chosen: plain JCS, declared in the record (canonicalization_id: "jcs") and checked before any digest is computed; null, empty arrays, and empty objects take part.
Rejected: a normalization pass that drops null or empty members (the withdrawn jcs-n), and any undeclared or unknown construction; these fail closed. (draft-mih-scitt-agent-action-capsule-05 §2)
Chosen: no floating-point values in digest-bearing material; integers outside the IEEE-754 safe range are decimal strings.
Rejected: JSON floats. JCS fixes one output per double, but two parsers can round the same literal to different doubles, and then no canonicalization helps. (draft-mih-scitt-agent-action-capsule-05 §2; draft-mih-sokolov-scitt-payload-binding-05 §12.3)
Chosen: for CBOR, a protected Hash Envelope header parameter, payload-preimage-encoding, whose value comes from a small registry; its initial entry is core-deterministic (RFC 8949 §4.2.1).
Rejected: a media-type parameter on preimage-content-type (259). application/cbor defines no parameters, so one would be needed per media type, and a CoAP Content-Format number per pairing. (COSE deterministic encodings, Introduction)
Chosen: a verifier that holds only the decoded value encodes it with the named encoding; if the parameter is absent or unknown, it reports the content binding as unverified.
Rejected: guessing the encoding from the content type or the shape of the value. A guess cannot tell a modified value from one encoded under a different rule. (COSE deterministic encodings, Verifying the Content Binding)

3What it shows / what it does not

Shows

  • That two independent implementations derive the same bytes, and so the same digest, from the same record when both apply the declared encoding.
  • Which key signed the 32-byte identity: the envelope signature verifies under the public key in kid.
  • Which encoding produced a Hash Envelope preimage, when payload-preimage-encoding is present.
  • That a digest mismatch under a declared encoding is not an encoder disagreement: the value differs, whether by edit or by a parser that read it differently (see the next column).

Does not show

  • Unicode equivalence. JCS does not normalize Unicode, so an NFC key and its NFD spelling are different records with different digests.
  • That a parser kept the value exactly. Decoding into native types can lose distinctions such as integer versus float, and then re-encoding cannot reproduce the preimage.
  • That the signing key is authorized for the operator; that is the caller's policy.
  • Anything when the verifier has neither the preimage nor the encoding: that case is unverified, never verified and never failed.
  • Secrecy. A deterministic digest is stable, so it is linkable wherever the content recurs, and low-entropy content can be guessed.

4Example

A conformance vector whose object keys sort differently under UTF-16 code units (what JCS requires) and under code points. The input carries the keys in JCS order; the expected result gives the canonical preimage and a recomputed capsule_id equal to the carried one.

input.json
{
  "action_id": "canonical-key-sort-utf16-vs-codepoint",
  …
  "canonicalization_id": "jcs",
  "capsule_id": "9d07be6c8b0cb9efafd089ee02a2e06846854ee70c3c0f56e88925c1d2cf5dde",
  …
  "extension": {
    "canonical_test_value": {
      "😀": "hi",
      "Z": "wide"
    }
  },
  "format_version": "4",
  …
}

expected.json
{
  "canonical_preimages": {
    "capsule_id": "{\"action_id\":\"canonical-key-sort-utf16-vs-codepoint\",…,\"extension\":{\"canonical_test_value\":{\"😀\":\"hi\",\"Z\":\"wide\"}},\"format_version\":\"4\",…}"
  },
  "capsule_id_recomputed": "9d07be6c8b0cb9efafd089ee02a2e06846854ee70c3c0f56e88925c1d2cf5dde",
  …
  "findings": [],
  "kind": "positive",
  "ok": true,
  "provenance": "spec-derived"
}
FieldTypeMeaning
canonicalization_idstringThe declared construction: plain RFC 8785 JCS.
capsule_id64 hexSHA-256 over the JCS bytes of the Capsule minus this field.
extension.canonical_test_valueobjectKeys U+1F600 (UTF-16 D83D DE00) and U+FF3A (FF3A); JCS puts the emoji ahead of Z.
format_versionstringSerialization suite "4", the only one supported.
canonical_preimages.capsule_idstringThe exact JCS string that was hashed (trimmed here).
capsule_id_recomputed64 hexThe verifier's own SHA-256 of that preimage; equals the carried value.
ok / findingsbool / arrayClass 1 verification passes with no findings.
provenancestring"spec-derived": "derived from the draft and RFC 8785 without running any repository implementation" (vectors/README.md).

Source: action-state-group/agent-action-capsule:vectors/capsule/canonical-key-sort-utf16-vs-codepoint/input.json and …/expected.json @ 2ef8aeb49e2334e24c404bd62d3fdb161770873c

5Specs