@@ -0,0 +1,83 @@
1+// COSE / CWT example program: produces a signed payload, a MACed
2+// payload and a signed CBOR Web Token, then verifies each.
3+//
4+// Run with: v run examples/cose_cwt.v
5+import encoding.cose
6+import encoding.cwt
7+import encoding.hex
8+
9+fn main() {
10+ demo_sign1_eddsa()!
11+ demo_mac0_hmac_256()!
12+ demo_signed_cwt_es256()!
13+}
14+
15+// COSE_Sign1 with EdDSA over Ed25519. EdDSA signatures are
16+// deterministic, so signing the same payload twice produces identical
17+// bytes — useful for caching and reproducible builds.
18+fn demo_sign1_eddsa() ! {
19+ d := hex.decode('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')!
20+ x := hex.decode('d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a')!
21+ priv := cose.Key.okp_private(.ed25519, x, d)
22+ pub_key := cose.Key.okp_public(.ed25519, x)
23+
24+ signed := cose.sign1('hello, COSE'.bytes(), priv,
25+ protected: cose.Headers{
26+ algorithm: .eddsa
27+ }
28+ unprotected: cose.Headers{
29+ kid: 'demo-key'.bytes()
30+ }
31+ )!
32+ println('Sign1 (${signed.len} bytes): ${hex.encode(signed)}')
33+
34+ payload := cose.verify1(signed, pub_key)!
35+ assert payload == 'hello, COSE'.bytes()
36+ println(' verified payload: ${payload.bytestr()}')
37+}
38+
39+// COSE_Mac0 with HMAC-SHA256. Symmetric, useful when both ends share
40+// a secret out of band (e.g. a session key).
41+fn demo_mac0_hmac_256() ! {
42+ key := cose.Key.symmetric([u8(0xA5)].repeat(32))
43+ maced := cose.mac0('mac me'.bytes(), key,
44+ protected: cose.Headers{
45+ algorithm: .hmac_256_256
46+ }
47+ )!
48+ println('Mac0 (${maced.len} bytes): ${hex.encode(maced)}')
49+
50+ got := cose.verify_mac0(maced, key)!
51+ assert got == 'mac me'.bytes()
52+ println(' verified payload: ${got.bytestr()}')
53+}
54+
55+// Signed CBOR Web Token (RFC 8392) wrapped in a COSE_Sign1, with
56+// ECDSA P-256 + SHA-256. Key material taken from RFC 8392 Appendix A.
57+fn demo_signed_cwt_es256() ! {
58+ x := hex.decode('143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f')!
59+ y := hex.decode('60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9')!
60+ d := hex.decode('6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19')!
61+ priv := cose.Key.ec2_private(.p_256, x, y, d)
62+ pub_key := cose.Key.ec2_public(.p_256, x, y)
63+
64+ claims := cwt.ClaimsSet{
65+ iss: 'coap://as.example.com'
66+ sub: 'erikw'
67+ aud: ['coap://light.example.com']
68+ exp: 1444064944
69+ iat: 1443944944
70+ }
71+ token := cwt.sign(claims, priv,
72+ protected: cose.Headers{
73+ algorithm: .es256
74+ }
75+ )!
76+ println('CWT (${token.len} bytes): ${hex.encode(token)}')
77+
78+ verified := cwt.verify(token, pub_key)!
79+ iss := verified.iss or { '' }
80+ sub := verified.sub or { '' }
81+ exp := verified.exp or { 0 }
82+ println(' iss=${iss} sub=${sub} exp=${exp}')
83+}
@@ -0,0 +1,188 @@
1+# `encoding.cose`
2+
3+CBOR Object Signing and Encryption — pure-V implementation of the
4+signing and MAC subset of [RFC 9052][rfc9052] and [RFC 9053][rfc9053].
5+
6+[rfc9052]: https://www.rfc-editor.org/rfc/rfc9052
7+[rfc9053]: https://www.rfc-editor.org/rfc/rfc9053
8+
9+## What it covers
10+
11+| Message type | Tag | Status |
12+| ---------------- | --- | ------ |
13+| `COSE_Sign1` | 18 | ✅ |
14+| `COSE_Sign` | 98 | ✅ |
15+| `COSE_Mac0` | 17 | ✅ |
16+| `COSE_Mac` | 97 | ✅ (direct mode) |
17+| `COSE_Encrypt0` | 16 | ❌ (needs AEAD primitives in `vlib/crypto`) |
18+| `COSE_Encrypt` | 96 | ❌ (idem) |
19+
20+| Algorithm | IANA | Status |
21+| ------------- | ---- | ------ |
22+| `ES256` | -7 | ✅ |
23+| `ES384` | -35 | ✅ |
24+| `ES512` | -36 | ✅ (P-521 + SHA-512) |
25+| `EdDSA` | -8 | ✅ Ed25519 |
26+| `HMAC 256/64` | 4 | ✅ |
27+| `HMAC 256/256`| 5 | ✅ |
28+| `HMAC 384/384`| 6 | ✅ |
29+| `HMAC 512/512`| 7 | ✅ |
30+
31+## Quick examples
32+
33+### Sign and verify a payload (COSE_Sign1, ES256)
34+
35+The most common pattern — single-signer ECDSA P-256 over SHA-256.
36+
37+```v
38+import encoding.cose
39+import encoding.hex
40+
41+fn main() {
42+ x := hex.decode('143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f')!
43+ y := hex.decode('60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9')!
44+ d := hex.decode('6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19')!
45+ priv := cose.Key.ec2_private(.p_256, x, y, d)
46+ pub_key := cose.Key.ec2_public(.p_256, x, y)
47+
48+ signed := cose.sign1('hello'.bytes(), priv,
49+ protected: cose.Headers{
50+ algorithm: .es256
51+ }
52+ )!
53+ payload := cose.verify1(signed, pub_key)!
54+ assert payload == 'hello'.bytes()
55+}
56+```
57+
58+### Sign and verify with EdDSA (Ed25519)
59+
60+EdDSA is deterministic — the same payload + key always produces the
61+same signature, useful for caching and reproducible builds.
62+
63+```v
64+import encoding.cose
65+import encoding.hex
66+
67+fn main() {
68+ d := hex.decode('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60')!
69+ x := hex.decode('d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a')!
70+ priv := cose.Key.okp_private(.ed25519, x, d)
71+ pub_key := cose.Key.okp_public(.ed25519, x)
72+
73+ signed := cose.sign1('hello'.bytes(), priv,
74+ protected: cose.Headers{
75+ algorithm: .eddsa
76+ }
77+ )!
78+ payload := cose.verify1(signed, pub_key)!
79+ assert payload == 'hello'.bytes()
80+}
81+```
82+
83+### MAC a payload (COSE_Mac0, HMAC-SHA256)
84+
85+```v
86+import encoding.cose
87+
88+fn main() {
89+ key := cose.Key.symmetric([u8(0x42)].repeat(32))
90+ tag := cose.mac0('payload'.bytes(), key,
91+ protected: cose.Headers{
92+ algorithm: .hmac_256_256
93+ }
94+ )!
95+ got := cose.verify_mac0(tag, key)!
96+ assert got == 'payload'.bytes()
97+}
98+```
99+
100+### Multi-signer (COSE_Sign)
101+
102+```v ignore
103+a := cose.Signer{
104+ key: alice_key
105+ protected: cose.Headers{ algorithm: .eddsa }
106+}
107+b := cose.Signer{
108+ key: bob_key
109+ protected: cose.Headers{ algorithm: .es256 }
110+}
111+msg := cose.sign('payload'.bytes(), [a, b])!
112+```
113+
114+## Cookbook
115+
116+### Verifying a webhook payload
117+
118+```v ignore
119+import encoding.cose
120+
121+fn handle_webhook(body []u8, pub_key cose.Key) ! {
122+ payload := cose.verify1(body, pub_key) or {
123+ match err {
124+ cose.VerificationFailed { return error('webhook signature invalid') }
125+ cose.MalformedMessage { return error('webhook bytes malformed') }
126+ else { return err }
127+ }
128+ }
129+ process(payload)!
130+}
131+```
132+
133+### Setting `kid` (key identifier) so the verifier can pick the right key
134+
135+The `kid` lives in the *unprotected* header — it's just routing info,
136+not security-critical, and can change without breaking the signature.
137+
138+```v ignore
139+signed := cose.sign1(body, priv,
140+ protected: cose.Headers{ algorithm: .es256 }
141+ unprotected: cose.Headers{ kid: 'k-2026-01'.bytes() }
142+)!
143+```
144+
145+On the verify side, decode first, look up the key by `kid`, then verify:
146+
147+```v ignore
148+msg := cose.Sign1Message.decode(signed)!
149+key := key_lookup(msg.unprotected.kid or { return error('no kid') })!
150+msg.verify(key, msg.payload or { []u8{} }, []u8{})!
151+```
152+
153+### Detached payload (sign without embedding the bytes)
154+
155+Useful when the payload is large or already transmitted out of band:
156+
157+```v ignore
158+sig_only := cose.sign1([]u8{}, priv,
159+ protected: cose.Headers{ algorithm: .es256 }
160+ detached_payload: large_blob
161+)!
162+// Later, on the receiving side:
163+cose.verify1(sig_only, pub_key, detached_payload: large_blob)!
164+```
165+
166+## API surface
167+
168+- `cose.Algorithm` — typed enum mapped to the IANA registry.
169+- `cose.Key` — COSE_Key with constructors `Key.ec2_*`, `Key.okp_*`,
170+ `Key.symmetric`. CBOR encode/decode via `key.encode()` /
171+ `Key.decode()`.
172+- `cose.Headers` — typed protected/unprotected header bag. Well-known
173+ parameters as fields, others via `extra_int_labels` /
174+ `extra_text_labels`. Always serialised in canonical CBOR order.
175+- `cose.sign1` / `cose.verify1` — single-signer convenience helpers.
176+- `cose.sign` / `cose.SignMessage` — multi-signer.
177+- `cose.mac0` / `cose.verify_mac0` — single-recipient MAC.
178+- `cose.mac` / `cose.verify_mac` — multi-recipient MAC (direct mode).
179+
180+Error variants: `VerificationFailed`, `MalformedMessage`,
181+`AlgorithmMismatch`, `UnsupportedAlgorithm`. Use `if err is X` to
182+discriminate.
183+
184+## See also
185+
186+- [`encoding.cbor`](../cbor/README.md) — the CBOR codec underneath.
187+- [`encoding.cwt`](../cwt/README.md) — CBOR Web Tokens (RFC 8392)
188+ built on top of this module.
@@ -0,0 +1,68 @@
1+// Algorithm identifiers from the IANA "COSE Algorithms" registry
2+// (https://www.iana.org/assignments/cose). Only the signature and MAC
3+// algorithms supported by the module are listed here; entries for
4+// AEAD encryption / RSA / key wrap will be added when those families
5+// land.
6+module cose
7+
8+// Algorithm is a COSE algorithm identifier as registered in the IANA
9+// "COSE Algorithms" registry. Values match the integer codes defined by
10+// RFC 9053.
11+pub enum Algorithm {
12+ // Signature algorithms (RFC 9053 §2)
13+ es256 = -7 // ECDSA w/ SHA-256, curve P-256
14+ es384 = -35 // ECDSA w/ SHA-384, curve P-384
15+ es512 = -36 // ECDSA w/ SHA-512, curve P-521
16+ eddsa = -8 // EdDSA (Ed25519 in this module)
17+
18+ // MAC algorithms (RFC 9053 §3)
19+ hmac_256_64 = 4 // HMAC w/ SHA-256, truncated to 64 bits
20+ hmac_256_256 = 5 // HMAC w/ SHA-256
21+ hmac_384_384 = 6 // HMAC w/ SHA-384
22+ hmac_512_512 = 7 // HMAC w/ SHA-512
23+}
24+
25+// is_signature reports whether the algorithm is a signature algorithm
26+// (used with COSE_Sign and COSE_Sign1).
27+pub fn (a Algorithm) is_signature() bool {
28+ return a in [.es256, .es384, .es512, .eddsa]
29+}
30+
31+// is_mac reports whether the algorithm is a MAC algorithm (used with
32+// COSE_Mac and COSE_Mac0).
33+pub fn (a Algorithm) is_mac() bool {
34+ return a in [.hmac_256_64, .hmac_256_256, .hmac_384_384, .hmac_512_512]
35+}
36+
37+// algorithm_from_int converts an IANA algorithm code to a typed
38+// Algorithm. It returns an error if the code is not supported by this
39+// module.
40+pub fn algorithm_from_int(code i64) !Algorithm {
41+ return match code {
42+ -7 { Algorithm.es256 }
43+ -35 { Algorithm.es384 }
44+ -36 { Algorithm.es512 }
45+ -8 { Algorithm.eddsa }
46+ 4 { Algorithm.hmac_256_64 }
47+ 5 { Algorithm.hmac_256_256 }
48+ 6 { Algorithm.hmac_384_384 }
49+ 7 { Algorithm.hmac_512_512 }
50+ else { error('cose: unsupported algorithm code ${code}') }
51+ }
52+}
53+
54+// name returns the IANA registered name of the algorithm (e.g. "ES256").
55+// Useful for error messages and logging; the wire format always uses the
56+// integer code.
57+pub fn (a Algorithm) name() string {
58+ return match a {
59+ .es256 { 'ES256' }
60+ .es384 { 'ES384' }
61+ .es512 { 'ES512' }
62+ .eddsa { 'EdDSA' }
63+ .hmac_256_64 { 'HMAC 256/64' }
64+ .hmac_256_256 { 'HMAC 256/256' }
65+ .hmac_384_384 { 'HMAC 384/384' }
66+ .hmac_512_512 { 'HMAC 512/512' }
67+ }
68+}
@@ -0,0 +1,182 @@
1+// Conversion between the DER-encoded ECDSA signatures produced by
2+// vlib/crypto/ecdsa (which delegates to OpenSSL) and the fixed-width
3+// `R || S` form mandated by COSE for ES256/ES384/ES512 (RFC 9053 §2.1).
4+//
5+// Wire format reminder (ASN.1 DER, X9.62 §5.4):
6+//
7+// SEQUENCE { 0x30 LEN
8+// INTEGER R 0x02 R_LEN R_BYTES
9+// INTEGER S 0x02 S_LEN S_BYTES
10+// }
11+//
12+// R and S are non-negative big-endian integers. DER requires the
13+// shortest encoding, so:
14+// - a leading 0x00 octet is present iff the next octet's MSB is 1
15+// (otherwise the value would be interpreted as negative);
16+// - leading 0x00 octets that are not required for sign disambiguation
17+// are forbidden.
18+//
19+// COSE raw form is just `R_padded || S_padded` where each integer is
20+// left-padded with zeros to `coordinate_size` bytes (32 / 48 / 66).
21+module cose
22+
23+// der_to_raw converts a DER-encoded ECDSA signature into the
24+// `R || S` fixed-width representation used by COSE.
25+fn der_to_raw(der []u8, coord_size int) ![]u8 {
26+ if der.len < 8 || der[0] != 0x30 {
27+ return MalformedMessage{
28+ reason: 'ECDSA DER: missing SEQUENCE tag'
29+ }
30+ }
31+ seq_len, body_start := read_der_length(der, 1)!
32+ if body_start + seq_len != der.len {
33+ return MalformedMessage{
34+ reason: 'ECDSA DER: SEQUENCE length mismatch'
35+ }
36+ }
37+ r, after_r := read_der_integer(der, body_start)!
38+ s, after_s := read_der_integer(der, after_r)!
39+ if after_s != der.len {
40+ return MalformedMessage{
41+ reason: 'ECDSA DER: trailing bytes'
42+ }
43+ }
44+
45+ mut out := []u8{len: 2 * coord_size}
46+ pad_left(mut out, 0, coord_size, r)!
47+ pad_left(mut out, coord_size, coord_size, s)!
48+ return out
49+}
50+
51+// raw_to_der converts a `R || S` COSE signature back to the DER form
52+// expected by vlib/crypto/ecdsa for verification.
53+fn raw_to_der(raw []u8, coord_size int) ![]u8 {
54+ if raw.len != 2 * coord_size {
55+ return MalformedMessage{
56+ reason: 'ECDSA raw: wrong length, expected ${2 * coord_size} got ${raw.len}'
57+ }
58+ }
59+ r := raw[..coord_size]
60+ s := raw[coord_size..]
61+ r_int := encode_der_integer(r)
62+ s_int := encode_der_integer(s)
63+ body_len := r_int.len + s_int.len
64+
65+ mut out := []u8{cap: 4 + body_len}
66+ out << 0x30
67+ out << encode_der_length(body_len)
68+ out << r_int
69+ out << s_int
70+ return out
71+}
72+
73+// read_der_length parses an ASN.1 length encoding starting at `idx` and
74+// returns (length, next_index). Supports both the short form (0..127)
75+// and the long form (0x81..0x84 prefix).
76+fn read_der_length(buf []u8, idx int) !(int, int) {
77+ if idx >= buf.len {
78+ return MalformedMessage{
79+ reason: 'DER: truncated length'
80+ }
81+ }
82+ first := buf[idx]
83+ if first < 0x80 {
84+ return int(first), idx + 1
85+ }
86+ n := int(first & 0x7f)
87+ if n == 0 || n > 4 {
88+ return MalformedMessage{
89+ reason: 'DER: invalid long-form length'
90+ }
91+ }
92+ if idx + 1 + n > buf.len {
93+ return MalformedMessage{
94+ reason: 'DER: truncated long-form length'
95+ }
96+ }
97+ mut length := u32(0)
98+ for i in 0 .. n {
99+ length = (length << u32(8)) | u32(buf[idx + 1 + i])
100+ }
101+ return int(length), idx + 1 + n
102+}
103+
104+// encode_der_length emits an ASN.1 length octet sequence. Used only for
105+// the SEQUENCE wrapper; the inner INTEGER lengths produced by
106+// `encode_der_integer` are always small enough for the short form.
107+fn encode_der_length(n int) []u8 {
108+ if n < 0x80 {
109+ return [u8(n)]
110+ }
111+ if n <= 0xff {
112+ return [u8(0x81), u8(n)]
113+ }
114+ if n <= 0xffff {
115+ return [u8(0x82), u8(n >> 8), u8(n)]
116+ }
117+ // P-521 max body is ~140 bytes — the >0xffff arms are never reached
118+ // in practice but kept defensive for completeness.
119+ return [u8(0x83), u8(n >> 16), u8(n >> 8), u8(n)]
120+}
121+
122+// read_der_integer parses one INTEGER TLV at `idx`, returning
123+// (raw magnitude bytes with leading sign-disambiguation 0x00 stripped,
124+// next_index after the TLV).
125+fn read_der_integer(buf []u8, idx int) !([]u8, int) {
126+ if idx + 2 > buf.len || buf[idx] != 0x02 {
127+ return MalformedMessage{
128+ reason: 'DER: missing INTEGER tag'
129+ }
130+ }
131+ length, body_start := read_der_length(buf, idx + 1)!
132+ if length <= 0 || body_start + length > buf.len {
133+ return MalformedMessage{
134+ reason: 'DER: bad INTEGER length'
135+ }
136+ }
137+ end := body_start + length
138+ mut start := body_start
139+ // Drop the leading 0x00 inserted purely to keep the value positive.
140+ if length > 1 && buf[start] == 0x00 && (buf[start + 1] & 0x80) != 0 {
141+ start++
142+ }
143+ return buf[start..end].clone(), end
144+}
145+
146+// encode_der_integer wraps `value` as an ASN.1 INTEGER TLV. Leading
147+// zeros in the magnitude are stripped, then a sign-disambiguation 0x00
148+// is prepended if the first remaining byte has its MSB set.
149+fn encode_der_integer(value []u8) []u8 {
150+ mut start := 0
151+ for start < value.len - 1 && value[start] == 0x00 {
152+ start++
153+ }
154+ payload := value[start..].clone()
155+ mut out := []u8{cap: 2 + payload.len + 1}
156+ out << 0x02
157+ if payload.len > 0 && (payload[0] & 0x80) != 0 {
158+ out << u8(payload.len + 1)
159+ out << 0x00
160+ out << payload
161+ } else {
162+ out << u8(payload.len)
163+ out << payload
164+ }
165+ return out
166+}
167+
168+// pad_left writes `value` into `out[off..off+width]` right-aligned, with
169+// zero left-padding. Errors out if `value` is too long for the width
170+// (which would mean the input integer doesn't fit the curve). Assumes
171+// `out` is already zero-filled (true for `[]u8{len: N}` allocations).
172+fn pad_left(mut out []u8, off int, width int, value []u8) ! {
173+ if value.len > width {
174+ return MalformedMessage{
175+ reason: 'ECDSA: integer larger than curve coordinate (${value.len} > ${width})'
176+ }
177+ }
178+ pad := width - value.len
179+ for i in 0 .. value.len {
180+ out[off + pad + i] = value[i]
181+ }
182+}
@@ -0,0 +1,136 @@
1+// Tests for ec_signature.v — DER ↔ raw R||S conversion. The reference
2+// values come from RFC 6979 (ECDSA deterministic signatures) and from
3+// hand-built DER blobs that exercise the leading-zero / MSB-set edge
4+// cases of the format.
5+module cose
6+
7+import encoding.hex
8+
9+fn test_der_to_raw_p256_roundtrip() {
10+ // R/S of length exactly 32 bytes, no DER padding needed.
11+ r := hex.decode('6520BBAF2081D7E0ED0F95F76EB0733D667005F7467CEC4B87B9381A6BA1EDE8')!
12+ s := hex.decode('00DF29F32A37230F39A842A54821FDD223092819D7728EFB9D3A0080B75380B')!
13+ mut raw := []u8{}
14+ raw << r
15+ raw << s
16+ assert raw.len == 64
17+
18+ der := raw_to_der(raw, 32)!
19+ back := der_to_raw(der, 32)!
20+ assert back == raw
21+}
22+
23+fn test_der_to_raw_strips_sign_byte() {
24+ // DER integer with leading 0x00 inserted because R's MSB is 1.
25+ // SEQUENCE (0x30) of two INTEGERs:
26+ // 0x02 0x21 0x00 R... (33-byte INTEGER, first content byte 0x00)
27+ // 0x02 0x20 S... (32-byte INTEGER)
28+ r_padded := hex.decode('00FFEEDDCCBBAA99887766554433221100FFEEDDCCBBAA99887766554433221100')!
29+ s := hex.decode('1122334455667788990011223344556677889900112233445566778899001122')!
30+ mut der := []u8{}
31+ der << 0x30
32+ der << u8(2 + r_padded.len + 2 + s.len)
33+ der << 0x02
34+ der << u8(r_padded.len)
35+ der << r_padded
36+ der << 0x02
37+ der << u8(s.len)
38+ der << s
39+ raw := der_to_raw(der, 32)!
40+ assert raw.len == 64
41+ // R is the original 32 high bytes (sign-disambiguation 0x00 dropped),
42+ // S is the 32-byte original.
43+ assert raw[..32] == r_padded[1..]
44+ assert raw[32..] == s
45+}
46+
47+fn test_raw_to_der_inserts_sign_byte_when_msb_set() {
48+ // Build a raw signature whose R has MSB set; the resulting DER must
49+ // contain a leading 0x00 to keep the integer positive.
50+ mut raw := []u8{len: 64}
51+ raw[0] = 0xff // R[0] MSB set
52+ raw[32] = 0x01 // S[0] no MSB
53+ der := raw_to_der(raw, 32)!
54+ // Find the first INTEGER content byte (after 0x30 LEN 0x02 INTLEN).
55+ assert der[0] == 0x30
56+ mut idx := 2 // skip SEQUENCE tag and length
57+ assert der[idx] == 0x02
58+ int_len := int(der[idx + 1])
59+ assert int_len == 33 // 32 bytes + leading 0x00
60+ assert der[idx + 2] == 0x00
61+ assert der[idx + 3] == 0xff
62+}
63+
64+fn test_raw_to_der_strips_internal_leading_zero() {
65+ // Raw R that already has a leading 0x00 (the high byte of the
66+ // curve scalar happens to be zero) must NOT keep it in DER beyond
67+ // what the sign-disambiguation rule requires.
68+ mut raw := []u8{len: 64}
69+ raw[1] = 0x42 // R = 00 42 00 ... — DER should encode as 0x02 0x1F 0x42 ...
70+ raw[32] = 0x01 // S = 01 00 ...
71+ der := raw_to_der(raw, 32)!
72+ assert der[0] == 0x30
73+ idx := 2
74+ assert der[idx] == 0x02
75+ int_len := int(der[idx + 1])
76+ // R magnitude after stripping leading zeros is 31 bytes: starts at
77+ // raw[1]=0x42 and ends at raw[31]=0x00.
78+ assert int_len == 31
79+ assert der[idx + 2] == 0x42
80+}
81+
82+fn test_der_to_raw_left_pads_short_integer() {
83+ // DER R with 30 effective bytes (leading zeros in scalar) must be
84+ // left-padded to 32 in the raw form.
85+ r_short := hex.decode('1122334455667788991122334455667788991122334455667788991122334455')!
86+ r_short_30 := r_short[2..] // 30 bytes
87+ s := hex.decode('1122334455667788990011223344556677889900112233445566778899001122')!
88+ mut der := []u8{}
89+ der << 0x30
90+ der << u8(2 + r_short_30.len + 2 + s.len)
91+ der << 0x02
92+ der << u8(r_short_30.len)
93+ der << r_short_30
94+ der << 0x02
95+ der << u8(s.len)
96+ der << s
97+ raw := der_to_raw(der, 32)!
98+ assert raw.len == 64
99+ assert raw[0] == 0x00
100+ assert raw[1] == 0x00
101+ assert raw[2..32] == r_short_30
102+}
103+
104+fn test_raw_to_der_rejects_wrong_length() {
105+ bad := []u8{len: 60} // P-256 expects 64
106+ if _ := raw_to_der(bad, 32) {
107+ assert false, 'should have rejected wrong-length raw'
108+ } else {
109+ assert err is MalformedMessage
110+ }
111+}
112+
113+fn test_der_to_raw_rejects_truncated() {
114+ bad := [u8(0x30), 0x05, 0x02, 0x01, 0x01]
115+ if _ := der_to_raw(bad, 32) {
116+ assert false, 'should have rejected truncated DER'
117+ } else {
118+ assert err is MalformedMessage
119+ }
120+}
121+
122+fn test_long_form_length_p521() {
123+ // P-521 signatures often exceed 0x7f bytes, so the SEQUENCE length
124+ // uses the long form (0x81 LEN). Build a minimal example and ensure
125+ // it round-trips.
126+ mut raw := []u8{len: 132}
127+ for i in 0 .. 132 {
128+ raw[i] = u8(i)
129+ }
130+ der := raw_to_der(raw, 66)!
131+ // The DER should start with 0x30 0x81 ... (long form, single byte).
132+ assert der[0] == 0x30
133+ assert der[1] == 0x81
134+ back := der_to_raw(der, 66)!
135+ assert back == raw
136+}
@@ -0,0 +1,69 @@
1+// Typed errors returned by this module. Callers can `match` on these
2+// variants (after `if err is X`) to react programmatically. Free-form
3+// errors continue to be returned via `error('...')` for diagnostic
4+// strings.
5+module cose
6+
7+// VerificationFailed is returned by verify routines when the signature
8+// or MAC tag does not match. Distinct from MalformedMessage so callers
9+// can tell a tampered/wrong-key message apart from a structurally
10+// invalid one.
11+pub struct VerificationFailed {
12+ Error
13+pub:
14+ // algorithm is the algorithm that was attempted, if known.
15+ algorithm ?Algorithm
16+}
17+
18+// msg formats a VerificationFailed for `IError.msg()`.
19+pub fn (e &VerificationFailed) msg() string {
20+ if alg := e.algorithm {
21+ return 'cose: verification failed (${alg.name()})'
22+ }
23+ return 'cose: verification failed'
24+}
25+
26+// MalformedMessage indicates the input bytes do not form a valid COSE
27+// message of the expected type. The `reason` describes which check
28+// failed; callers should treat this as a permanent error.
29+pub struct MalformedMessage {
30+ Error
31+pub:
32+ reason string
33+}
34+
35+// msg formats a MalformedMessage for `IError.msg()`.
36+pub fn (e &MalformedMessage) msg() string {
37+ return 'cose: malformed message: ${e.reason}'
38+}
39+
40+// AlgorithmMismatch is returned when a key constrains itself to one
41+// algorithm via its `alg` parameter and the caller asks the module
42+// to use a different one. This catches the common mistake of passing
43+// e.g. an ES256-only key to an EdDSA signing call.
44+pub struct AlgorithmMismatch {
45+ Error
46+pub:
47+ expected Algorithm // algorithm declared by the key
48+ got Algorithm // algorithm requested for the operation
49+}
50+
51+// msg formats an AlgorithmMismatch for `IError.msg()`.
52+pub fn (e &AlgorithmMismatch) msg() string {
53+ return 'cose: key declares alg=${e.expected.name()} but operation requested ${e.got.name()}'
54+}
55+
56+// UnsupportedAlgorithm is returned when an operation is attempted with
57+// an algorithm that the called function does not handle (e.g. a MAC
58+// algorithm passed to a signing routine).
59+pub struct UnsupportedAlgorithm {
60+ Error
61+pub:
62+ algorithm Algorithm
63+ context string // e.g. "signing", "MAC"
64+}
65+
66+// msg formats an UnsupportedAlgorithm for `IError.msg()`.
67+pub fn (e &UnsupportedAlgorithm) msg() string {
68+ return 'cose: ${e.algorithm.name()} cannot be used for ${e.context}'
69+}
@@ -0,0 +1,411 @@
1+// Header parameters as defined by RFC 9052 §3 ("Header Parameters") and
2+// the IANA "COSE Header Parameters" registry. The well-known integer
3+// labels relevant to signature and MAC processing are exposed as typed
4+// fields on `Headers`; callers can carry additional parameters
5+// (private-use, experimental, application-specific) through
6+// `extra_int_labels` and `extra_text_labels`.
7+module cose
8+
9+import encoding.cbor
10+
11+// Well-known integer labels from RFC 9052 §3.1, table 2.
12+// Kept as private constants so the on-the-wire encoding can change
13+// independently from the public API.
14+const label_alg = i64(1)
15+const label_crit = i64(2)
16+const label_content_type = i64(3)
17+const label_kid = i64(4)
18+const label_iv = i64(5)
19+const label_partial_iv = i64(6)
20+
21+// Headers carries the header parameters of a COSE message. Each COSE
22+// message has two header buckets: the *protected* bucket (integrity-
23+// covered) and the *unprotected* bucket (informational only). Both use
24+// this type — the bucket they live in is decided by the message struct,
25+// not by Headers itself.
26+//
27+// All well-known parameters are optional. To omit a parameter from the
28+// encoded output, leave its field as `none` (or empty for the slice
29+// fields). Use `extra_int_labels` / `extra_text_labels` for parameters
30+// not modelled here.
31+pub struct Headers {
32+pub mut:
33+ // algorithm — label 1.
34+ algorithm ?Algorithm
35+ // critical — label 2. Lists integer labels that MUST appear in the
36+ // protected header and that the recipient MUST understand. Text-
37+ // labelled crit entries are accepted on decode (kept in the cbor
38+ // `Value` of the surrounding map) but not modelled here, since
39+ // real-world COSE deployments always use integer labels.
40+ critical []i64
41+ // content_type — label 3. RFC 9052 allows either a uint (CoAP
42+ // content-format) or a tstr (IANA media type). Set at most one of
43+ // these two fields; if both are set, the int form wins on encode.
44+ content_type_int ?u64
45+ content_type_text ?string
46+ // kid — label 4. Application-level key identifier. Octet string.
47+ kid ?[]u8
48+ // iv — label 5. Full initialization vector for AEAD/MAC-with-IV
49+ // algorithms. Modelled here so messages produced by other COSE
50+ // implementations round-trip cleanly even though the current set of
51+ // supported algorithms (HMAC variants) does not consume it.
52+ iv ?[]u8
53+ // partial_iv — label 6. Partial IV; XOR'd with the context IV to
54+ // derive the effective IV.
55+ partial_iv ?[]u8
56+ // extra_int_labels carries integer-labelled parameters not covered
57+ // above. Order is preserved on encode so callers can produce stable
58+ // output, though COSE itself does not require any particular order
59+ // in *un*protected headers. Protected headers are always re-sorted
60+ // canonically on encode regardless.
61+ extra_int_labels []HeaderEntry
62+ // extra_text_labels carries text-labelled parameters (private use).
63+ extra_text_labels []TextHeaderEntry
64+}
65+
66+// HeaderEntry is one (int label, value) pair. The value is held as a
67+// cbor.Value so any CBOR datum can be carried.
68+pub struct HeaderEntry {
69+pub:
70+ label i64
71+ value cbor.Value
72+}
73+
74+// TextHeaderEntry is one (text label, value) pair.
75+pub struct TextHeaderEntry {
76+pub:
77+ label string
78+ value cbor.Value
79+}
80+
81+// is_empty reports whether the Headers contains no parameters at all.
82+// An empty *protected* Headers serialises to a zero-length bstr (`0x40`)
83+// per RFC 9052 §3.
84+pub fn (h Headers) is_empty() bool {
85+ return h.algorithm == none && h.critical.len == 0 && h.content_type_int == none
86+ && h.content_type_text == none && h.kid == none && h.iv == none && h.partial_iv == none
87+ && h.extra_int_labels.len == 0 && h.extra_text_labels.len == 0
88+}
89+
90+// to_value returns the Headers as a `cbor.Value` (always a `Map`),
91+// suitable for embedding in another CBOR structure via
92+// `Packer.pack_value`. Pair order is the same as on the wire after
93+// canonical sorting (the Packer sorts when `canonical = true`).
94+pub fn (h Headers) to_value() cbor.Value {
95+ mut pairs := []cbor.MapPair{cap: 7 + h.extra_int_labels.len + h.extra_text_labels.len}
96+ h.append_pairs(mut pairs)
97+ return cbor.Map{
98+ pairs: pairs
99+ }
100+}
101+
102+// encode_map returns the canonical CBOR encoding of the Headers as a
103+// CBOR map (definite length, sorted keys per RFC 8949 §4.2.1). This is
104+// the form used inside both the protected bstr wrapper and the
105+// unprotected slot.
106+pub fn (h Headers) encode_map() ![]u8 {
107+ return cbor.encode(h.to_value(), cbor.EncodeOpts{ canonical: true })!
108+}
109+
110+// append_pairs writes the CBOR map pairs for this Headers into `pairs`.
111+// Used internally by `to_value` and `encode_map` to share construction
112+// logic without allocating intermediates.
113+fn (h Headers) append_pairs(mut pairs []cbor.MapPair) {
114+ if alg := h.algorithm {
115+ pairs << cbor.MapPair{
116+ key: cbor.new_int(label_alg)
117+ value: cbor.new_int(i64(alg))
118+ }
119+ }
120+ if h.critical.len > 0 {
121+ mut crit_arr := cbor.Array{}
122+ for c in h.critical {
123+ crit_arr.elements << cbor.new_int(c)
124+ }
125+ pairs << cbor.MapPair{
126+ key: cbor.new_int(label_crit)
127+ value: crit_arr
128+ }
129+ }
130+ // content_type: uint takes precedence over tstr if both are set
131+ if ct := h.content_type_int {
132+ pairs << cbor.MapPair{
133+ key: cbor.new_int(label_content_type)
134+ value: cbor.new_uint(ct)
135+ }
136+ } else if ct := h.content_type_text {
137+ pairs << cbor.MapPair{
138+ key: cbor.new_int(label_content_type)
139+ value: cbor.new_text(ct)
140+ }
141+ }
142+ if kid := h.kid {
143+ pairs << cbor.MapPair{
144+ key: cbor.new_int(label_kid)
145+ value: cbor.new_bytes(kid)
146+ }
147+ }
148+ if iv := h.iv {
149+ pairs << cbor.MapPair{
150+ key: cbor.new_int(label_iv)
151+ value: cbor.new_bytes(iv)
152+ }
153+ }
154+ if piv := h.partial_iv {
155+ pairs << cbor.MapPair{
156+ key: cbor.new_int(label_partial_iv)
157+ value: cbor.new_bytes(piv)
158+ }
159+ }
160+ for e in h.extra_int_labels {
161+ pairs << cbor.MapPair{
162+ key: cbor.new_int(e.label)
163+ value: e.value
164+ }
165+ }
166+ for e in h.extra_text_labels {
167+ pairs << cbor.MapPair{
168+ key: cbor.new_text(e.label)
169+ value: e.value
170+ }
171+ }
172+}
173+
174+// encode_protected returns the bstr-wrapped canonical CBOR encoding of
175+// the protected headers, as used in the wire message (a CBOR byte string
176+// containing either an empty buffer or the CBOR map). RFC 9052 §3:
177+// "the empty map is encoded as a zero-length string rather than as a
178+// h'A0'".
179+pub fn (h Headers) encode_protected() ![]u8 {
180+ if h.is_empty() {
181+ return []u8{}
182+ }
183+ return h.encode_map()!
184+}
185+
186+// parse_headers_map decodes a CBOR map (already extracted from the
187+// surrounding message) into a Headers value. Unknown labels go into
188+// `extra_int_labels` / `extra_text_labels` instead of being dropped, so
189+// round-trips preserve the original parameters.
190+pub fn parse_headers_map(data []u8) !Headers {
191+ if data.len == 0 {
192+ return Headers{}
193+ }
194+ v := cbor.decode[cbor.Value](data, cbor.DecodeOpts{})!
195+ return parse_headers_value(v)!
196+}
197+
198+// parse_headers_value is the same as parse_headers_map but takes an
199+// already-decoded cbor.Value. Used internally when the surrounding
200+// decoder has already consumed the bytes.
201+fn parse_headers_value(v cbor.Value) !Headers {
202+ if v is cbor.Map {
203+ mut h := Headers{}
204+ mut seen_int_labels := []i64{}
205+ mut seen_text_labels := []string{}
206+ for pair in v.pairs {
207+ if int_key := pair.key.as_int() {
208+ if int_key in seen_int_labels {
209+ return MalformedMessage{
210+ reason: 'duplicate header label ${int_key} (RFC 9052 §3)'
211+ }
212+ }
213+ seen_int_labels << int_key
214+ match int_key {
215+ label_alg {
216+ code := pair.value.as_int() or {
217+ return MalformedMessage{
218+ reason: 'alg label has non-integer value'
219+ }
220+ }
221+ // Don't hard-fail when the algorithm isn't one
222+ // of the IANA values we model: it might be a
223+ // recipient routing marker (e.g. direct = -6
224+ // in COSE_Mac), or an algorithm we just don't
225+ // support yet. Surface it as an extra label
226+ // so the rest of the message can still be
227+ // inspected; high-level sign/verify routines
228+ // will return a clear error if the algorithm
229+ // is needed and unknown.
230+ if alg := algorithm_from_int(code) {
231+ h.algorithm = alg
232+ } else {
233+ h.extra_int_labels << HeaderEntry{
234+ label: label_alg
235+ value: pair.value
236+ }
237+ }
238+ }
239+ label_crit {
240+ items := pair.value.as_array() or {
241+ return MalformedMessage{
242+ reason: 'crit label is not an array'
243+ }
244+ }
245+ mut crit := []i64{cap: items.len}
246+ for item in items {
247+ c := item.as_int() or {
248+ return MalformedMessage{
249+ reason: 'crit array contains non-integer'
250+ }
251+ }
252+ crit << c
253+ }
254+ h.critical = crit
255+ }
256+ label_content_type {
257+ if u := pair.value.as_uint() {
258+ h.content_type_int = u
259+ } else if s := pair.value.as_string() {
260+ h.content_type_text = s
261+ } else {
262+ return MalformedMessage{
263+ reason: 'content type is neither uint nor tstr'
264+ }
265+ }
266+ }
267+ label_kid {
268+ b := pair.value.as_bytes() or {
269+ return MalformedMessage{
270+ reason: 'kid is not a byte string'
271+ }
272+ }
273+ h.kid = b
274+ }
275+ label_iv {
276+ b := pair.value.as_bytes() or {
277+ return MalformedMessage{
278+ reason: 'iv is not a byte string'
279+ }
280+ }
281+ h.iv = b
282+ }
283+ label_partial_iv {
284+ b := pair.value.as_bytes() or {
285+ return MalformedMessage{
286+ reason: 'partial iv is not a byte string'
287+ }
288+ }
289+ h.partial_iv = b
290+ }
291+ else {
292+ h.extra_int_labels << HeaderEntry{
293+ label: int_key
294+ value: pair.value
295+ }
296+ }
297+ }
298+ } else if str_key := pair.key.as_string() {
299+ if str_key in seen_text_labels {
300+ return MalformedMessage{
301+ reason: 'duplicate header label "${str_key}" (RFC 9052 §3)'
302+ }
303+ }
304+ seen_text_labels << str_key
305+ h.extra_text_labels << TextHeaderEntry{
306+ label: str_key
307+ value: pair.value
308+ }
309+ } else {
310+ return MalformedMessage{
311+ reason: 'header label is neither int nor tstr'
312+ }
313+ }
314+ }
315+ return h
316+ }
317+ return MalformedMessage{
318+ reason: 'header bucket is not a CBOR map'
319+ }
320+}
321+
322+// parse_protected decodes a protected header bstr (the unwrapped bytes,
323+// not the bstr itself). An empty buffer maps to an empty Headers.
324+pub fn parse_protected(data []u8) !Headers {
325+ if data.len == 0 {
326+ return Headers{}
327+ }
328+ return parse_headers_map(data)!
329+}
330+
331+// check_protected_headers enforces RFC 9052 §3.1 bucket rules for
332+// protected/unprotected headers. `crit` itself must be integrity-protected,
333+// and every label listed in `crit` must also be present in the protected
334+// bucket.
335+fn check_protected_headers(protected Headers, unprotected Headers) ! {
336+ if unprotected.critical.len > 0 {
337+ return MalformedMessage{
338+ reason: 'crit label must be in protected headers (RFC 9052 §3.1)'
339+ }
340+ }
341+ check_critical(protected)!
342+ for label in protected.critical {
343+ if !has_int_label(protected, label) {
344+ return MalformedMessage{
345+ reason: 'crit lists label ${label}, but it is not present in protected headers (RFC 9052 §3.1)'
346+ }
347+ }
348+ }
349+}
350+
351+// check_critical enforces RFC 9052 §3.1: every integer label listed in
352+// `crit` MUST be one the receiver understands; otherwise verification
353+// MUST fail. We "understand" the IANA-registered labels 1..6 modelled
354+// by typed fields; any other label in `crit` is treated as a hard
355+// error to avoid silently ignoring a parameter the sender flagged as
356+// security-critical.
357+fn check_critical(h Headers) ! {
358+ for label in h.critical {
359+ if label !in [label_alg, label_crit, label_content_type, label_kid, label_iv,
360+ label_partial_iv] {
361+ return MalformedMessage{
362+ reason: 'crit lists unknown label ${label} (RFC 9052 §3.1)'
363+ }
364+ }
365+ }
366+}
367+
368+// has_int_label reports whether `h` already declares the given integer
369+// label, either via a typed well-known field or via `extra_int_labels`.
370+fn has_int_label(h Headers, label i64) bool {
371+ match label {
372+ label_alg {
373+ if h.algorithm != none {
374+ return true
375+ }
376+ }
377+ label_crit {
378+ if h.critical.len > 0 {
379+ return true
380+ }
381+ }
382+ label_content_type {
383+ if h.content_type_int != none || h.content_type_text != none {
384+ return true
385+ }
386+ }
387+ label_kid {
388+ if h.kid != none {
389+ return true
390+ }
391+ }
392+ label_iv {
393+ if h.iv != none {
394+ return true
395+ }
396+ }
397+ label_partial_iv {
398+ if h.partial_iv != none {
399+ return true
400+ }
401+ }
402+ else {}
403+ }
404+
405+ for e in h.extra_int_labels {
406+ if e.label == label {
407+ return true
408+ }
409+ }
410+ return false
411+}
@@ -0,0 +1,132 @@
1+// Tests for the Headers struct: encoding, decoding, canonical map order,
2+// extra labels, content-type variants and the "empty protected = empty
3+// bstr" rule of RFC 9052 §3.
4+module cose
5+
6+import encoding.cbor
7+import encoding.hex
8+
9+fn test_empty_protected_is_zero_length() {
10+ h := Headers{}
11+ assert h.is_empty()
12+ enc := h.encode_protected()!
13+ assert enc.len == 0
14+}
15+
16+fn test_alg_only_protected_matches_es256_vector() {
17+ mut h := Headers{}
18+ h.algorithm = .es256
19+ enc := h.encode_protected()!
20+ // canonical CBOR map with one (1 -> -7) pair: A1 01 26
21+ assert enc == hex.decode('A10126')!
22+}
23+
24+fn test_alg_and_ctyp_protected_matches_ecdsa01_vector() {
25+ mut h := Headers{}
26+ h.algorithm = .es256
27+ h.content_type_int = u64(0)
28+ enc := h.encode_protected()!
29+ // A2 01 26 03 00
30+ assert enc == hex.decode('A201260300')!
31+}
32+
33+fn test_canonical_sorts_keys() {
34+ // Add labels out of order; canonical encoding must reorder them.
35+ mut h := Headers{}
36+ h.kid = 'k'.bytes()
37+ h.algorithm = .es256
38+ enc := h.encode_protected()!
39+ // kid (4) must come AFTER alg (1) in canonical order.
40+ // A2 01 26 04 41 6B
41+ assert enc == hex.decode('A201260441'.to_upper() + '6B')!
42+}
43+
44+fn test_roundtrip_kid_and_alg() {
45+ mut h := Headers{}
46+ h.algorithm = .es256
47+ h.kid = 'my-key'.bytes()
48+ enc := h.encode_protected()!
49+ parsed := parse_protected(enc)!
50+ assert parsed.algorithm == ?Algorithm(.es256)
51+ assert (parsed.kid or { []u8{} }) == 'my-key'.bytes()
52+}
53+
54+fn test_unknown_int_label_preserved_on_roundtrip() {
55+ mut h := Headers{}
56+ h.algorithm = .es256
57+ h.extra_int_labels << HeaderEntry{
58+ label: 99
59+ value: cbor.new_text('hello')
60+ }
61+ enc := h.encode_protected()!
62+ parsed := parse_protected(enc)!
63+ assert parsed.algorithm == ?Algorithm(.es256)
64+ assert parsed.extra_int_labels.len == 1
65+ assert parsed.extra_int_labels[0].label == 99
66+ assert (parsed.extra_int_labels[0].value.as_string() or { '' }) == 'hello'
67+}
68+
69+fn test_text_label_preserved_on_roundtrip() {
70+ mut h := Headers{}
71+ h.extra_text_labels << TextHeaderEntry{
72+ label: 'app-id'
73+ value: cbor.new_int(42)
74+ }
75+ enc := h.encode_protected()!
76+ parsed := parse_protected(enc)!
77+ assert parsed.extra_text_labels.len == 1
78+ assert parsed.extra_text_labels[0].label == 'app-id'
79+ assert (parsed.extra_text_labels[0].value.as_int() or { 0 }) == 42
80+}
81+
82+fn test_content_type_text_form_roundtrips() {
83+ mut h := Headers{}
84+ h.content_type_text = 'application/cbor'
85+ enc := h.encode_protected()!
86+ parsed := parse_protected(enc)!
87+ assert (parsed.content_type_text or { '' }) == 'application/cbor'
88+ assert parsed.content_type_int == none
89+}
90+
91+fn test_critical_array_roundtrips() {
92+ mut h := Headers{}
93+ h.algorithm = .es256
94+ h.critical = [i64(99), i64(100)]
95+ enc := h.encode_protected()!
96+ parsed := parse_protected(enc)!
97+ assert parsed.critical == [i64(99), i64(100)]
98+}
99+
100+fn test_unknown_alg_falls_back_to_extra_label() {
101+ // 1 -> -1000 is a valid CBOR map but not a known alg. Per
102+ // RFC 9052 §3 we should preserve the parameter rather than reject
103+ // the whole header — high-level sign/verify routines will fail
104+ // later with a clear error if the algorithm is actually needed.
105+ bad := hex.decode('A10139' + '03E7')! // -1000 = negative arg 999 (0x03E7)
106+ parsed := parse_protected(bad)!
107+ assert parsed.algorithm == none
108+ assert parsed.extra_int_labels.len == 1
109+ assert parsed.extra_int_labels[0].label == 1
110+}
111+
112+fn test_parse_rejects_duplicate_int_labels() {
113+ // map with duplicate alg labels: {1: -7, 1: -8}
114+ dup := hex.decode('A201260127')!
115+ if _ := parse_protected(dup) {
116+ assert false, 'duplicate integer labels must be rejected'
117+ } else {
118+ assert err is MalformedMessage
119+ assert err.msg().contains('duplicate header label 1')
120+ }
121+}
122+
123+fn test_parse_rejects_duplicate_text_labels() {
124+ // map with duplicate text labels: {"x": 0, "x": 1}
125+ dup := hex.decode('A2617800617801')!
126+ if _ := parse_protected(dup) {
127+ assert false, 'duplicate text labels must be rejected'
128+ } else {
129+ assert err is MalformedMessage
130+ assert err.msg().contains('duplicate header label "x"')
131+ }
132+}
@@ -0,0 +1,423 @@
1+// COSE_Key as defined by RFC 9052 §7 and the IANA "COSE Key Types" /
2+// "COSE Elliptic Curves" registries. EC2, OKP and Symmetric keys are
3+// covered (enough for ES256/384/512, EdDSA and HMAC); RSA support is
4+// gated on RSA primitives in `vlib/crypto`.
5+module cose
6+
7+import encoding.cbor
8+
9+// Common COSE_Key parameter labels (RFC 9052 §7.1, table 4).
10+const key_label_kty = i64(1)
11+const key_label_kid = i64(2)
12+const key_label_alg = i64(3)
13+const key_label_key_ops = i64(4)
14+const key_label_base_iv = i64(5)
15+
16+// Type-specific key parameter labels (RFC 9053 §7.1 / §7.2 / §6.1).
17+const key_label_crv = i64(-1) // EC2, OKP
18+const key_label_x = i64(-2) // EC2 (x-coord), OKP (public key)
19+const key_label_y = i64(-3) // EC2 (y-coord, can be bool for compressed)
20+const key_label_d = i64(-4) // EC2, OKP (private key)
21+const key_label_k = i64(-1) // Symmetric
22+
23+// KeyType identifies the cryptographic family of a COSE_Key (label 1).
24+// Values match the IANA "COSE Key Types" registry.
25+pub enum KeyType {
26+ okp = 1 // Octet Key Pair (Ed25519, X25519…)
27+ ec2 = 2 // Elliptic Curve, two-coordinate
28+ rsa = 3 // RSA — not yet supported by this module
29+ symmetric = 4
30+}
31+
32+// Curve identifies an elliptic curve used by EC2 or OKP keys (label
33+// -1 of the type-specific parameters). Only the curves actually used
34+// by this module's algorithms are listed; others can still be parsed
35+// but are reported as unsupported when a key is converted to a
36+// signer/verifier.
37+pub enum Curve {
38+ p_256 = 1 // EC2, ES256
39+ p_384 = 2 // EC2, ES384
40+ p_521 = 3 // EC2, ES512 (note: 521-bit, not 512)
41+ ed25519 = 6 // OKP, EdDSA
42+}
43+
44+// KeyOp restricts the operations a key may be used for (label 4).
45+// Values match the IANA "COSE Key Operation Values" registry.
46+pub enum KeyOp {
47+ sign = 1
48+ verify = 2
49+ encrypt = 3
50+ decrypt = 4
51+ wrap_key = 5
52+ unwrap_key = 6
53+ derive_key = 7
54+ derive_bits = 8
55+ mac_create = 9
56+ mac_verify = 10
57+}
58+
59+// Key is the V representation of a COSE_Key. Fields applicable to the
60+// `kty` are populated; others stay `none`. Use the typed constructors
61+// (`Key.ec2_*`, `Key.okp_*`, `Key.symmetric`) rather than building
62+// instances by hand — they enforce the invariants of each key type.
63+pub struct Key {
64+pub mut:
65+ kty KeyType
66+ kid ?[]u8
67+ alg ?Algorithm
68+ key_ops []KeyOp
69+ base_iv ?[]u8
70+
71+ // EC2 / OKP:
72+ crv ?Curve
73+ x ?[]u8 // EC2 x-coordinate, or OKP public key
74+ y ?[]u8 // EC2 y-coordinate
75+ d ?[]u8 // private scalar (optional)
76+
77+ // Symmetric:
78+ k ?[]u8
79+}
80+
81+// Key.ec2_private builds an EC2 private key from raw coordinates and
82+// scalar. `x` and `y` are the public point components (big-endian, no
83+// leading 0x00 padding required), `d` is the private scalar.
84+pub fn Key.ec2_private(crv Curve, x []u8, y []u8, d []u8) Key {
85+ return Key{
86+ kty: .ec2
87+ crv: crv
88+ x: x
89+ y: y
90+ d: d
91+ }
92+}
93+
94+// Key.ec2_public builds an EC2 public key (no private scalar).
95+pub fn Key.ec2_public(crv Curve, x []u8, y []u8) Key {
96+ return Key{
97+ kty: .ec2
98+ crv: crv
99+ x: x
100+ y: y
101+ }
102+}
103+
104+// Key.okp_private builds an OKP private key. For Ed25519, `x` is the
105+// 32-byte public key and `d` is the 32-byte private seed.
106+pub fn Key.okp_private(crv Curve, x []u8, d []u8) Key {
107+ return Key{
108+ kty: .okp
109+ crv: crv
110+ x: x
111+ d: d
112+ }
113+}
114+
115+// Key.okp_public builds an OKP public key.
116+pub fn Key.okp_public(crv Curve, x []u8) Key {
117+ return Key{
118+ kty: .okp
119+ crv: crv
120+ x: x
121+ }
122+}
123+
124+// Key.symmetric builds a Symmetric key from raw key material.
125+pub fn Key.symmetric(k []u8) Key {
126+ return Key{
127+ kty: .symmetric
128+ k: k
129+ }
130+}
131+
132+// encode returns the canonical CBOR encoding of the COSE_Key.
133+pub fn (k Key) encode() ![]u8 {
134+ mut pairs := []cbor.MapPair{cap: 8}
135+ pairs << cbor.MapPair{
136+ key: cbor.new_int(key_label_kty)
137+ value: cbor.new_int(i64(k.kty))
138+ }
139+ if kid := k.kid {
140+ pairs << cbor.MapPair{
141+ key: cbor.new_int(key_label_kid)
142+ value: cbor.new_bytes(kid)
143+ }
144+ }
145+ if alg := k.alg {
146+ pairs << cbor.MapPair{
147+ key: cbor.new_int(key_label_alg)
148+ value: cbor.new_int(i64(alg))
149+ }
150+ }
151+ if k.key_ops.len > 0 {
152+ mut ops_arr := cbor.Array{}
153+ for op in k.key_ops {
154+ ops_arr.elements << cbor.new_int(i64(op))
155+ }
156+ pairs << cbor.MapPair{
157+ key: cbor.new_int(key_label_key_ops)
158+ value: ops_arr
159+ }
160+ }
161+ if biv := k.base_iv {
162+ pairs << cbor.MapPair{
163+ key: cbor.new_int(key_label_base_iv)
164+ value: cbor.new_bytes(biv)
165+ }
166+ }
167+
168+ match k.kty {
169+ .symmetric {
170+ km := k.k or { return error('cose: symmetric key missing k parameter') }
171+ pairs << cbor.MapPair{
172+ key: cbor.new_int(key_label_k)
173+ value: cbor.new_bytes(km)
174+ }
175+ }
176+ .ec2, .okp {
177+ crv := k.crv or { return error('cose: ${k.kty} key missing crv parameter') }
178+ x := k.x or { return error('cose: ${k.kty} key missing x parameter') }
179+ pairs << cbor.MapPair{
180+ key: cbor.new_int(key_label_crv)
181+ value: cbor.new_int(i64(crv))
182+ }
183+ pairs << cbor.MapPair{
184+ key: cbor.new_int(key_label_x)
185+ value: cbor.new_bytes(x)
186+ }
187+ if k.kty == .ec2 {
188+ y := k.y or { return error('cose: EC2 key missing y parameter') }
189+ pairs << cbor.MapPair{
190+ key: cbor.new_int(key_label_y)
191+ value: cbor.new_bytes(y)
192+ }
193+ }
194+ if d := k.d {
195+ pairs << cbor.MapPair{
196+ key: cbor.new_int(key_label_d)
197+ value: cbor.new_bytes(d)
198+ }
199+ }
200+ }
201+ .rsa {
202+ return error('cose: RSA keys are not supported in this module version')
203+ }
204+ }
205+
206+ return cbor.encode(cbor.Value(cbor.Map{ pairs: pairs }), cbor.EncodeOpts{
207+ canonical: true
208+ })!
209+}
210+
211+// Key.decode parses a CBOR-encoded COSE_Key.
212+pub fn Key.decode(data []u8) !Key {
213+ v := cbor.decode[cbor.Value](data, cbor.DecodeOpts{})!
214+ m := if v is cbor.Map {
215+ v
216+ } else {
217+ return MalformedMessage{
218+ reason: 'COSE_Key is not a CBOR map'
219+ }
220+ }
221+
222+ mut out := Key{}
223+ mut found_kty := false
224+ for pair in m.pairs {
225+ int_key := pair.key.as_int() or {
226+ // Text labels for keys are private use; we silently ignore
227+ // them on decode rather than failing — they don't affect
228+ // crypto operations.
229+ continue
230+ }
231+ match int_key {
232+ key_label_kty {
233+ code := pair.value.as_int() or {
234+ return MalformedMessage{
235+ reason: 'kty is not an integer'
236+ }
237+ }
238+ out.kty = match code {
239+ 1 {
240+ KeyType.okp
241+ }
242+ 2 {
243+ KeyType.ec2
244+ }
245+ 3 {
246+ KeyType.rsa
247+ }
248+ 4 {
249+ KeyType.symmetric
250+ }
251+ else {
252+ return MalformedMessage{
253+ reason: 'unknown kty ${code}'
254+ }
255+ }
256+ }
257+
258+ found_kty = true
259+ }
260+ key_label_kid {
261+ out.kid = pair.value.as_bytes() or {
262+ return MalformedMessage{
263+ reason: 'kid is not bstr'
264+ }
265+ }
266+ }
267+ key_label_alg {
268+ code := pair.value.as_int() or {
269+ return MalformedMessage{
270+ reason: 'alg is not int'
271+ }
272+ }
273+ // Lenient: an unknown algorithm leaves `alg` as `none`
274+ // so the rest of the key (kid, public material, …)
275+ // remains usable. Symmetric with the Headers parser.
276+ if alg := algorithm_from_int(code) {
277+ out.alg = alg
278+ }
279+ }
280+ key_label_key_ops {
281+ items := pair.value.as_array() or {
282+ return MalformedMessage{
283+ reason: 'key_ops is not array'
284+ }
285+ }
286+ mut ops := []KeyOp{cap: items.len}
287+ for it in items {
288+ n := it.as_int() or {
289+ return MalformedMessage{
290+ reason: 'key_ops contains non-int'
291+ }
292+ }
293+ ops << match n {
294+ 1 {
295+ KeyOp.sign
296+ }
297+ 2 {
298+ KeyOp.verify
299+ }
300+ 3 {
301+ KeyOp.encrypt
302+ }
303+ 4 {
304+ KeyOp.decrypt
305+ }
306+ 5 {
307+ KeyOp.wrap_key
308+ }
309+ 6 {
310+ KeyOp.unwrap_key
311+ }
312+ 7 {
313+ KeyOp.derive_key
314+ }
315+ 8 {
316+ KeyOp.derive_bits
317+ }
318+ 9 {
319+ KeyOp.mac_create
320+ }
321+ 10 {
322+ KeyOp.mac_verify
323+ }
324+ else {
325+ return MalformedMessage{
326+ reason: 'unknown key_op ${n}'
327+ }
328+ }
329+ }
330+ }
331+ out.key_ops = ops
332+ }
333+ key_label_base_iv {
334+ out.base_iv = pair.value.as_bytes() or {
335+ return MalformedMessage{
336+ reason: 'base_iv is not bstr'
337+ }
338+ }
339+ }
340+ else {
341+ // Type-specific parameters are interpreted after kty is
342+ // known, in a second pass below.
343+ }
344+ }
345+ }
346+ if !found_kty {
347+ return MalformedMessage{
348+ reason: 'COSE_Key missing kty'
349+ }
350+ }
351+
352+ for pair in m.pairs {
353+ int_key := pair.key.as_int() or { continue }
354+ match out.kty {
355+ .symmetric {
356+ if int_key == key_label_k {
357+ out.k = pair.value.as_bytes() or {
358+ return MalformedMessage{
359+ reason: 'k is not bstr'
360+ }
361+ }
362+ }
363+ }
364+ .ec2, .okp {
365+ match int_key {
366+ key_label_crv {
367+ code := pair.value.as_int() or {
368+ return MalformedMessage{
369+ reason: 'crv is not int'
370+ }
371+ }
372+ out.crv = match code {
373+ 1 {
374+ Curve.p_256
375+ }
376+ 2 {
377+ Curve.p_384
378+ }
379+ 3 {
380+ Curve.p_521
381+ }
382+ 6 {
383+ Curve.ed25519
384+ }
385+ else {
386+ return MalformedMessage{
387+ reason: 'unsupported crv ${code}'
388+ }
389+ }
390+ }
391+ }
392+ key_label_x {
393+ out.x = pair.value.as_bytes() or {
394+ return MalformedMessage{
395+ reason: 'x is not bstr'
396+ }
397+ }
398+ }
399+ key_label_y {
400+ // RFC 9053 §7.1.1 also allows a boolean `y` for
401+ // compressed points; that form is not supported.
402+ out.y = pair.value.as_bytes() or {
403+ return MalformedMessage{
404+ reason: 'y is not bstr (compressed points not supported)'
405+ }
406+ }
407+ }
408+ key_label_d {
409+ out.d = pair.value.as_bytes() or {
410+ return MalformedMessage{
411+ reason: 'd is not bstr'
412+ }
413+ }
414+ }
415+ else {}
416+ }
417+ }
418+ .rsa {}
419+ }
420+ }
421+
422+ return out
423+}
@@ -0,0 +1,247 @@
1+// COSE_Mac — multi-recipient MACed message — RFC 9052 §6.1.
2+//
3+// COSE_Mac carries a single MAC tag computed over the body, plus an
4+// array of per-recipient envelopes that describe how each recipient
5+// derives or wraps the MAC key. Only the "direct" recipient mode
6+// (RFC 9053 §6.1.1) is supported: each recipient is identified by
7+// `kid` and is assumed to share the symmetric key out of band, so the
8+// `encrypted_key` slot of every recipient is empty. The other
9+// recipient modes (key wrap, key derivation) require AEAD primitives
10+// that are not yet wired up.
11+module cose
12+
13+import encoding.cbor
14+
15+// alg_direct is the COSE algorithm value for "direct" key derivation
16+// (RFC 9053 §6.1.1, IANA "COSE Algorithms" registry).
17+const alg_direct = i64(-6)
18+
19+// max_recipients caps the `recipients` array size on decode. See the
20+// rationale on `max_signers` in sign.v.
21+const max_recipients = 256
22+
23+// Recipient is one entry of the `recipients` array of a COSE_Mac
24+// message. In "direct" mode the recipient carries only routing info
25+// (typically `kid` plus `alg = direct` in the unprotected header) and
26+// an empty `encrypted_key`.
27+pub struct Recipient {
28+pub mut:
29+ protected Headers
30+ unprotected Headers
31+ encrypted_key []u8
32+}
33+
34+// MacMessage is the V representation of a COSE_Mac message.
35+pub struct MacMessage {
36+pub mut:
37+ protected Headers
38+ unprotected Headers
39+ payload ?[]u8
40+ tag []u8
41+ recipients []Recipient
42+}
43+
44+// MacOptions bundles inputs to `cose.mac`.
45+@[params]
46+pub struct MacOptions {
47+pub:
48+ protected Headers
49+ unprotected Headers
50+ external_aad []u8
51+ detached_payload ?[]u8
52+ untagged bool
53+ // recipients: at least one entry. Each entry SHOULD set its
54+ // unprotected `kid` so that the receiver can pick the right shared
55+ // key. The `alg = direct (-6)` parameter is auto-added if absent.
56+ recipients []Recipient
57+}
58+
59+// VerifyMacOptions bundles inputs to `cose.verify_mac`.
60+@[params]
61+pub struct VerifyMacOptions {
62+pub:
63+ external_aad []u8
64+ detached_payload ?[]u8
65+}
66+
67+// mac produces a tagged COSE_Mac message. The MAC tag is computed
68+// once, over the body — recipients are descriptive routing only in
69+// "direct" mode. The body algorithm in `opts.protected.algorithm`
70+// drives the MAC computation and the symmetric `key` is the shared
71+// secret named by the recipients' `kid`.
72+pub fn mac(payload []u8, key Key, opts MacOptions) ![]u8 {
73+ if opts.recipients.len == 0 {
74+ return error('cose: COSE_Mac requires at least one recipient')
75+ }
76+ alg := opts.protected.algorithm or {
77+ return error('cose: COSE_Mac requires protected.algorithm to be set')
78+ }
79+
80+ signed_bytes := opts.detached_payload or { payload }
81+ body_protected := opts.protected.encode_protected()!
82+ tbm := mac_structure_mac(body_protected, opts.external_aad, signed_bytes)
83+ tag := compute_mac(alg, key, tbm)!
84+
85+ // Build the on-wire recipients without mutating the caller's input
86+ // (struct copy + per-recipient header normalisation).
87+ mut recipients := []Recipient{cap: opts.recipients.len}
88+ for src in opts.recipients {
89+ mut new_unprotected := src.unprotected
90+ if new_unprotected.algorithm == none && !has_int_label(new_unprotected, label_alg) {
91+ // Re-allocate the slice so we don't mutate the caller's
92+ // Headers if it shares its backing array.
93+ mut extras := []HeaderEntry{cap: new_unprotected.extra_int_labels.len + 1}
94+ extras << new_unprotected.extra_int_labels
95+ extras << HeaderEntry{
96+ label: label_alg
97+ value: cbor.new_int(alg_direct)
98+ }
99+ new_unprotected.extra_int_labels = extras
100+ }
101+ recipients << Recipient{
102+ protected: src.protected
103+ unprotected: new_unprotected
104+ encrypted_key: []u8{} // direct mode → empty bstr
105+ }
106+ }
107+
108+ mut msg := MacMessage{
109+ protected: opts.protected
110+ unprotected: opts.unprotected
111+ payload: payload
112+ tag: tag
113+ recipients: recipients
114+ }
115+ if opts.detached_payload != none {
116+ msg.payload = none
117+ }
118+ return msg.encode(!opts.untagged)!
119+}
120+
121+// verify_mac parses a COSE_Mac, recomputes the MAC tag with `key` and
122+// checks it. Returns the payload bytes. The algorithm MUST be present
123+// in the protected headers per RFC 9052 §3.
124+pub fn verify_mac(message []u8, key Key, opts VerifyMacOptions) ![]u8 {
125+ msg := MacMessage.decode(message)!
126+ check_protected_headers(msg.protected, msg.unprotected)!
127+ for r in msg.recipients {
128+ check_protected_headers(r.protected, r.unprotected)!
129+ }
130+ pl := if dp := opts.detached_payload {
131+ dp
132+ } else {
133+ msg.payload or {
134+ return MalformedMessage{
135+ reason: 'detached payload requires VerifyMacOptions.detached_payload'
136+ }
137+ }
138+ }
139+ alg := msg.protected.algorithm or {
140+ return MalformedMessage{
141+ reason: 'algorithm missing from protected header (RFC 9052 §3)'
142+ }
143+ }
144+
145+ body_protected := msg.protected.encode_protected()!
146+ tbm := mac_structure_mac(body_protected, opts.external_aad, pl)
147+ mac_verify(alg, key, tbm, msg.tag)!
148+ return pl
149+}
150+
151+// encode serialises the MacMessage. When `tagged` is true the output
152+// is wrapped in CBOR tag 97.
153+pub fn (m MacMessage) encode(tagged bool) ![]u8 {
154+ body_protected := m.protected.encode_protected()!
155+
156+ mut p := cbor.new_packer(cbor.EncodeOpts{ canonical: true })
157+ if tagged {
158+ p.pack_tag(tag_mac)
159+ }
160+ p.pack_array_header(5)
161+ p.pack_bytes(body_protected)
162+ p.pack_value(m.unprotected.to_value())!
163+ if pl := m.payload {
164+ p.pack_bytes(pl)
165+ } else {
166+ p.pack_null()
167+ }
168+ p.pack_bytes(m.tag)
169+ p.pack_array_header(u64(m.recipients.len))
170+ for r in m.recipients {
171+ rp := r.protected.encode_protected()!
172+ p.pack_array_header(3)
173+ p.pack_bytes(rp)
174+ p.pack_value(r.unprotected.to_value())!
175+ p.pack_bytes(r.encrypted_key)
176+ }
177+ return p.bytes()
178+}
179+
180+// MacMessage.decode parses a CBOR-encoded COSE_Mac.
181+pub fn MacMessage.decode(data []u8) !MacMessage {
182+ mut u := cbor.new_unpacker(data, cbor.DecodeOpts{})
183+ if u.peek_kind()! == .tag_val {
184+ tag_no := u.unpack_tag()!
185+ if tag_no != tag_mac {
186+ return MalformedMessage{
187+ reason: 'expected tag ${tag_mac} (Mac), got ${tag_no}'
188+ }
189+ }
190+ }
191+ header_count := u.unpack_array_header()!
192+ if header_count != 5 {
193+ return MalformedMessage{
194+ reason: 'Mac array must have 5 elements, got ${header_count}'
195+ }
196+ }
197+
198+ protected := parse_protected(u.unpack_bytes()!)!
199+ unprotected := parse_headers_value(u.unpack_value()!)!
200+ mut payload := ?[]u8(none)
201+ if u.peek_kind()! == .null_val {
202+ u.unpack_null()!
203+ } else {
204+ payload = u.unpack_bytes()!
205+ }
206+ tag := u.unpack_bytes()!
207+
208+ recipients_count := u.unpack_array_header()!
209+ if recipients_count <= 0 {
210+ return MalformedMessage{
211+ reason: 'Mac requires at least one recipient, got ${recipients_count}'
212+ }
213+ }
214+ if recipients_count > max_recipients {
215+ return MalformedMessage{
216+ reason: 'Mac claims ${recipients_count} recipients (over ${max_recipients}-entry sanity cap)'
217+ }
218+ }
219+ mut recipients := []Recipient{cap: int(recipients_count)}
220+ for _ in 0 .. recipients_count {
221+ if u.unpack_array_header()! != 3 {
222+ return MalformedMessage{
223+ reason: 'COSE_recipient array must have 3 elements'
224+ }
225+ }
226+ r_protected := parse_protected(u.unpack_bytes()!)!
227+ r_unprotected := parse_headers_value(u.unpack_value()!)!
228+ ek := u.unpack_bytes()!
229+ recipients << Recipient{
230+ protected: r_protected
231+ unprotected: r_unprotected
232+ encrypted_key: ek
233+ }
234+ }
235+ if !u.done() {
236+ return MalformedMessage{
237+ reason: 'trailing bytes after Mac'
238+ }
239+ }
240+ return MacMessage{
241+ protected: protected
242+ unprotected: unprotected
243+ payload: payload
244+ tag: tag
245+ recipients: recipients
246+ }
247+}
@@ -0,0 +1,157 @@
1+// COSE_Mac0 — single-recipient MACed message — RFC 9052 §6.2.
2+module cose
3+
4+import encoding.cbor
5+
6+// Mac0Message is the V representation of a COSE_Mac0 message. The
7+// fields map 1:1 to the CBOR array slots defined by RFC 9052 §6.2.
8+pub struct Mac0Message {
9+pub mut:
10+ protected Headers
11+ unprotected Headers
12+ // payload — `none` means a detached payload (the actual payload is
13+ // transmitted out of band).
14+ payload ?[]u8
15+ tag []u8
16+}
17+
18+// Mac0Options bundles the parameters that drive `cose.mac0`.
19+@[params]
20+pub struct Mac0Options {
21+pub:
22+ protected Headers
23+ unprotected Headers
24+ external_aad []u8
25+ detached_payload ?[]u8
26+ untagged bool
27+}
28+
29+// VerifyMac0Options bundles inputs to `cose.verify_mac0`.
30+@[params]
31+pub struct VerifyMac0Options {
32+pub:
33+ external_aad []u8
34+ detached_payload ?[]u8
35+}
36+
37+// mac0 produces a tagged COSE_Mac0 message in one call.
38+pub fn mac0(payload []u8, key Key, opts Mac0Options) ![]u8 {
39+ mut msg := Mac0Message{
40+ protected: opts.protected
41+ unprotected: opts.unprotected
42+ payload: payload
43+ }
44+ pl := if dp := opts.detached_payload {
45+ dp
46+ } else {
47+ payload
48+ }
49+ msg.compute(key, pl, opts.external_aad)!
50+ if opts.detached_payload != none {
51+ msg.payload = none
52+ }
53+ return msg.encode(!opts.untagged)!
54+}
55+
56+// verify_mac0 parses a (tagged or untagged) COSE_Mac0, verifies the tag
57+// against `key`, and returns the payload.
58+pub fn verify_mac0(message []u8, key Key, opts VerifyMac0Options) ![]u8 {
59+ msg := Mac0Message.decode(message)!
60+ pl := if dp := opts.detached_payload {
61+ dp
62+ } else {
63+ msg.payload or {
64+ return MalformedMessage{
65+ reason: 'detached payload requires VerifyMac0Options.detached_payload'
66+ }
67+ }
68+ }
69+ msg.verify(key, pl, opts.external_aad)!
70+ return pl
71+}
72+
73+// compute computes the MAC tag and stores it in `tag`.
74+pub fn (mut m Mac0Message) compute(key Key, payload []u8, external_aad []u8) ! {
75+ alg := m.protected.algorithm or {
76+ return error('cose: Mac0Message.compute requires protected.algorithm to be set')
77+ }
78+
79+ body_protected := m.protected.encode_protected()!
80+ tbm := mac_structure_mac0(body_protected, external_aad, payload)
81+ m.tag = compute_mac(alg, key, tbm)!
82+}
83+
84+// verify recomputes the MAC tag and checks it against the stored one.
85+pub fn (m Mac0Message) verify(key Key, payload []u8, external_aad []u8) ! {
86+ check_protected_headers(m.protected, m.unprotected)!
87+ alg := m.protected.algorithm or {
88+ return MalformedMessage{
89+ reason: 'algorithm missing from protected header (RFC 9052 §3)'
90+ }
91+ }
92+
93+ body_protected := m.protected.encode_protected()!
94+ tbm := mac_structure_mac0(body_protected, external_aad, payload)
95+ mac_verify(alg, key, tbm, m.tag)!
96+}
97+
98+// encode serialises the message. When `tagged` is true the output is
99+// wrapped in CBOR tag 17 (RFC 9052 §2).
100+pub fn (m Mac0Message) encode(tagged bool) ![]u8 {
101+ body_protected := m.protected.encode_protected()!
102+
103+ mut p := cbor.new_packer(cbor.EncodeOpts{ canonical: true })
104+ if tagged {
105+ p.pack_tag(tag_mac0)
106+ }
107+ p.pack_array_header(4)
108+ p.pack_bytes(body_protected)
109+ p.pack_value(m.unprotected.to_value())!
110+ if pl := m.payload {
111+ p.pack_bytes(pl)
112+ } else {
113+ p.pack_null()
114+ }
115+ p.pack_bytes(m.tag)
116+ return p.bytes()
117+}
118+
119+// Mac0Message.decode parses a CBOR-encoded COSE_Mac0.
120+pub fn Mac0Message.decode(data []u8) !Mac0Message {
121+ mut u := cbor.new_unpacker(data, cbor.DecodeOpts{})
122+ if u.peek_kind()! == .tag_val {
123+ tag_no := u.unpack_tag()!
124+ if tag_no != tag_mac0 {
125+ return MalformedMessage{
126+ reason: 'expected tag ${tag_mac0} (Mac0), got ${tag_no}'
127+ }
128+ }
129+ }
130+ header_count := u.unpack_array_header()!
131+ if header_count != 4 {
132+ return MalformedMessage{
133+ reason: 'Mac0 array must have 4 elements, got ${header_count}'
134+ }
135+ }
136+
137+ protected := parse_protected(u.unpack_bytes()!)!
138+ unprotected := parse_headers_value(u.unpack_value()!)!
139+ mut payload := ?[]u8(none)
140+ if u.peek_kind()! == .null_val {
141+ u.unpack_null()!
142+ } else {
143+ payload = u.unpack_bytes()!
144+ }
145+ tag := u.unpack_bytes()!
146+ if !u.done() {
147+ return MalformedMessage{
148+ reason: 'trailing bytes after Mac0'
149+ }
150+ }
151+ return Mac0Message{
152+ protected: protected
153+ unprotected: unprotected
154+ payload: payload
155+ tag: tag
156+ }
157+}
@@ -0,0 +1,135 @@
1+// Tests for COSE_Mac0. HMAC tags are deterministic, so every test can
2+// match the reference vector bytes-exactly.
3+module cose
4+
5+import encoding.base64
6+import encoding.hex
7+
8+// HMAC-enc-01.json from cose-wg/Examples (Unlicense): HS256 mac0,
9+// implicit "direct" recipient, empty unprotected, no external AAD.
10+const hmac_hs256_key_b64u = 'hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg'
11+const hmac_enc01_message = 'D18443A10105A054546869732069732074686520636F6E74656E742E5820A1A848D3471F9D61EE49018D244C824772F223AD4F935293F1789FC3A08D8C58'
12+
13+const sample_text = 'This is the content.'
14+
15+fn test_mac0_hs256_matches_reference_vector() {
16+ k := base64.url_decode(hmac_hs256_key_b64u)
17+ key := Key.symmetric(k)
18+ mut hp := Headers{}
19+ hp.algorithm = .hmac_256_256
20+ got := mac0(sample_text.bytes(), key, protected: hp)!
21+ want := hex.decode(hmac_enc01_message)!
22+ assert got == want
23+}
24+
25+fn test_verify_mac0_accepts_reference_vector() {
26+ k := base64.url_decode(hmac_hs256_key_b64u)
27+ key := Key.symmetric(k)
28+ msg := hex.decode(hmac_enc01_message)!
29+ payload := verify_mac0(msg, key)!
30+ assert payload == sample_text.bytes()
31+}
32+
33+fn test_mac0_truncated_hs256_64_tag_size() {
34+ key := Key.symmetric([u8(0x42)].repeat(32))
35+ mut hp := Headers{}
36+ hp.algorithm = .hmac_256_64
37+ signed := mac0('hi'.bytes(), key, protected: hp)!
38+ msg := Mac0Message.decode(signed)!
39+ // HMAC 256/64 truncates to 8 bytes per RFC 9053 §3.1.
40+ assert msg.tag.len == 8
41+ got := verify_mac0(signed, key)!
42+ assert got == 'hi'.bytes()
43+}
44+
45+fn test_mac0_hs384_roundtrip() {
46+ key := Key.symmetric([u8(0x33)].repeat(48))
47+ mut hp := Headers{}
48+ hp.algorithm = .hmac_384_384
49+ signed := mac0('hello'.bytes(), key, protected: hp)!
50+ msg := Mac0Message.decode(signed)!
51+ assert msg.tag.len == 48
52+ got := verify_mac0(signed, key)!
53+ assert got == 'hello'.bytes()
54+}
55+
56+fn test_mac0_hs512_roundtrip() {
57+ key := Key.symmetric([u8(0x77)].repeat(64))
58+ mut hp := Headers{}
59+ hp.algorithm = .hmac_512_512
60+ signed := mac0('hello'.bytes(), key, protected: hp)!
61+ msg := Mac0Message.decode(signed)!
62+ assert msg.tag.len == 64
63+ got := verify_mac0(signed, key)!
64+ assert got == 'hello'.bytes()
65+}
66+
67+// HMAC-ENC-02: HS384 mac0 reference vector (cose-wg/Examples).
68+fn test_mac0_hs384_matches_reference_vector() {
69+ k := base64.url_decode('hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYgAESIzd4iZqiEiIyQlJico')
70+ key := Key.symmetric(k)
71+ mut hp := Headers{}
72+ hp.algorithm = .hmac_384_384
73+ got := mac0(sample_text.bytes(), key, protected: hp)!
74+ want :=
75+ hex.decode('D18443A10106A054546869732069732074686520636F6E74656E742E5830998D26C6459AAEECF44ED20CE00C8CCEDF0A1F3D22A92FC05DB08C5AEB1CB594CAAF5A5C5E2E9D01CCE7E77A93AA8C62')!
76+ assert got == want
77+}
78+
79+// HMAC-ENC-03: HS512 mac0 reference vector (cose-wg/Examples).
80+fn test_mac0_hs512_matches_reference_vector() {
81+ k :=
82+ base64.url_decode('hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYgAESIzd4iZqiEiIyQlJicoqrvM3e7_paanqKmgsbKztA')
83+ key := Key.symmetric(k)
84+ mut hp := Headers{}
85+ hp.algorithm = .hmac_512_512
86+ got := mac0(sample_text.bytes(), key, protected: hp)!
87+ want :=
88+ hex.decode('D18443A10107A054546869732069732074686520636F6E74656E742E58404A555BF971F7C1891D9DDF304A1A132E2D6F817449474D813E6D04D65962BED8BBA70C17E1F5308FA39962959A4B9B8D7DA8E6D849B209DCD3E98CC0F11EDDF2')!
89+ assert got == want
90+}
91+
92+fn test_verify_mac0_rejects_wrong_key() {
93+ wrong := Key.symmetric([u8(0x00)].repeat(32))
94+ msg := hex.decode(hmac_enc01_message)!
95+ if _ := verify_mac0(msg, wrong) {
96+ assert false, 'wrong key must not verify'
97+ } else {
98+ assert err is VerificationFailed
99+ }
100+}
101+
102+fn test_verify_mac0_rejects_tampered_tag() {
103+ k := base64.url_decode(hmac_hs256_key_b64u)
104+ key := Key.symmetric(k)
105+ mut msg := hex.decode(hmac_enc01_message)!
106+ // Last byte is the tail of the tag bstr; flip a bit.
107+ msg[msg.len - 1] ^= 0x01
108+ if _ := verify_mac0(msg, key) {
109+ assert false, 'tampered tag must not verify'
110+ } else {
111+ assert err is VerificationFailed
112+ }
113+}
114+
115+fn test_mac0_external_aad_is_authenticated() {
116+ k := base64.url_decode(hmac_hs256_key_b64u)
117+ key := Key.symmetric(k)
118+ mut hp := Headers{}
119+ hp.algorithm = .hmac_256_256
120+ a := mac0(sample_text.bytes(), key, protected: hp)!
121+ b := mac0(sample_text.bytes(), key, protected: hp, external_aad: 'context'.bytes())!
122+ assert a != b
123+}
124+
125+fn test_mac0_detached_payload() {
126+ k := base64.url_decode(hmac_hs256_key_b64u)
127+ key := Key.symmetric(k)
128+ mut hp := Headers{}
129+ hp.algorithm = .hmac_256_256
130+ signed := mac0([]u8{}, key, protected: hp, detached_payload: 'remote'.bytes())!
131+ msg := Mac0Message.decode(signed)!
132+ assert msg.payload == none
133+ got := verify_mac0(signed, key, detached_payload: 'remote'.bytes())!
134+ assert got == 'remote'.bytes()
135+}
@@ -0,0 +1,90 @@
1+// Tests for COSE_Mac — multi-recipient MACed messages (direct mode).
2+module cose
3+
4+import encoding.base64
5+
6+fn test_mac_direct_recipient_roundtrip() {
7+ k := base64.url_decode('hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg')
8+ key := Key.symmetric(k)
9+ mut hp := Headers{}
10+ hp.algorithm = .hmac_256_256
11+ mut hu_recip := Headers{}
12+ hu_recip.kid = 'our-secret'.bytes()
13+ recip := Recipient{
14+ unprotected: hu_recip
15+ }
16+ signed := mac('This is the content.'.bytes(), key,
17+ protected: hp
18+ recipients: [
19+ recip,
20+ ]
21+ )!
22+ msg := MacMessage.decode(signed)!
23+ assert msg.recipients.len == 1
24+ assert msg.tag.len == 32
25+ got := verify_mac(signed, key)!
26+ assert got == 'This is the content.'.bytes()
27+}
28+
29+fn test_mac_rejects_no_recipients() {
30+ k := base64.url_decode('hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg')
31+ key := Key.symmetric(k)
32+ mut hp := Headers{}
33+ hp.algorithm = .hmac_256_256
34+ if _ := mac('payload'.bytes(), key, protected: hp) {
35+ assert false, 'must reject zero recipients'
36+ } else {
37+ assert err.msg().contains('at least one recipient')
38+ }
39+}
40+
41+fn test_mac_decode_rejects_huge_recipients_count() {
42+ // COSE_Mac with a recipients-array length declared as 4 billion;
43+ // the sanity cap must reject it without allocating that much memory.
44+ mut bad := []u8{}
45+ bad << 0xD8 // tag
46+ bad << 0x61 // tag 97
47+ bad << 0x85 // array(5)
48+ bad << 0x40 // bstr(0) — protected
49+ bad << 0xA0 // map(0) — unprotected
50+ bad << 0x40 // bstr(0) — payload
51+ bad << 0x40 // bstr(0) — tag
52+ // recipients array header: 0x9A 0xFFFFFFFF
53+ bad << 0x9A
54+ bad << 0xFF
55+ bad << 0xFF
56+ bad << 0xFF
57+ bad << 0xFF
58+ if _ := MacMessage.decode(bad) {
59+ assert false, 'must reject huge recipients count'
60+ } else {
61+ assert err is MalformedMessage
62+ }
63+}
64+
65+fn test_mac_recipient_alg_direct_auto_added() {
66+ k := base64.url_decode('hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg')
67+ key := Key.symmetric(k)
68+ mut hp := Headers{}
69+ hp.algorithm = .hmac_256_256
70+ mut hu_recip := Headers{}
71+ hu_recip.kid = 'r1'.bytes()
72+ recip := Recipient{
73+ unprotected: hu_recip
74+ }
75+ signed := mac('p'.bytes(), key, protected: hp, recipients: [recip])!
76+ msg := MacMessage.decode(signed)!
77+ // The recipient's unprotected header must carry alg = direct (-6).
78+ first := msg.recipients[0]
79+ mut found_direct := false
80+ for e in first.unprotected.extra_int_labels {
81+ if e.label == 1 {
82+ if v := e.value.as_int() {
83+ if v == -6 {
84+ found_direct = true
85+ }
86+ }
87+ }
88+ }
89+ assert found_direct
90+}
@@ -0,0 +1,69 @@
1+// Algorithm-aware MAC computation. Sibling of `signing.v`. Only this
2+// file imports `crypto.hmac` and the SHA-2 modules; the rest of the
3+// MAC handling code goes through `compute_mac` / `verify_mac`.
4+module cose
5+
6+import crypto.hmac
7+import crypto.sha256
8+import crypto.sha512
9+
10+// compute_mac returns the MAC tag (already truncated for HMAC 256/64)
11+// for `data` under `alg` using the symmetric `key`.
12+fn compute_mac(alg Algorithm, key Key, data []u8) ![]u8 {
13+ if !alg.is_mac() {
14+ return UnsupportedAlgorithm{
15+ algorithm: alg
16+ context: 'MAC'
17+ }
18+ }
19+ if key_alg := key.alg {
20+ if key_alg != alg {
21+ return AlgorithmMismatch{
22+ expected: key_alg
23+ got: alg
24+ }
25+ }
26+ }
27+ if key.kty != .symmetric {
28+ return error('cose: ${alg.name()} requires kty=Symmetric, got ${key.kty}')
29+ }
30+ k := key.k or { return error('cose: symmetric key missing k') }
31+
32+ tag := match alg {
33+ .hmac_256_64, .hmac_256_256 {
34+ hmac.new(k, data, sha256.sum, sha256.block_size)
35+ }
36+ .hmac_384_384 {
37+ hmac.new(k, data, sha512.sum384, sha512.block_size)
38+ }
39+ .hmac_512_512 {
40+ hmac.new(k, data, sha512.sum512, sha512.block_size)
41+ }
42+ else {
43+ // Unreachable: `alg.is_mac()` above ruled out non-MAC values
44+ // and the four MAC variants are all enumerated. Kept so the
45+ // match stays exhaustive over the Algorithm enum.
46+ return UnsupportedAlgorithm{
47+ algorithm: alg
48+ context: 'MAC'
49+ }
50+ }
51+ }
52+
53+ // HMAC 256/64 (RFC 9053 §3.1) truncates to the leftmost 8 bytes.
54+ if alg == .hmac_256_64 {
55+ return tag[..8].clone()
56+ }
57+ return tag
58+}
59+
60+// mac_verify checks that `tag` matches the MAC of `data` under `alg`
61+// and `key`, using a constant-time comparison.
62+fn mac_verify(alg Algorithm, key Key, data []u8, tag []u8) ! {
63+ expected := compute_mac(alg, key, data)!
64+ if !hmac.equal(expected, tag) {
65+ return VerificationFailed{
66+ algorithm: alg
67+ }
68+ }
69+}
@@ -0,0 +1,67 @@
1+// Tests for ES384 and ES512 (P-384 and P-521 curves), validating that
2+// the DER ↔ R||S conversion handles the wider integer widths correctly.
3+// Sign + verify roundtrip is the right invariant since ECDSA signatures
4+// are randomised.
5+module cose
6+
7+import encoding.base64
8+import encoding.hex
9+
10+// ecdsa-sig-02 (P-384) reference message and key.
11+const p384_x_b64u = 'kTJyP2KSsBBhnb4kjWmMF7WHVsY55xUPgb7k64rDcjatChoZ1nvjKmYmPh5STRKc'
12+const p384_y_b64u = 'mM0weMVU2DKsYDxDJkEP9hZiRZtB8fPfXbzINZj_fF7YQRynNWedHEyzAJOX2e8s'
13+const p384_d_b64u = 'ok3Nq97AXlpEusO7jIy1FZATlBP9PNReMU7DWbkLQ5dU90snHuuHVDjEPmtV0fTo'
14+const p384_sig02_message = 'D28444A1013822A104445033383454546869732069732074686520636F6E74656E742E58605F150ABD1C7D25B32065A14E05D6CB1F665D10769FF455EA9A2E0ADAB5DE63838DB257F0949C41E13330E110EBA7B912F34E1546FB1366A2568FAA91EC3E6C8D42F4A67A0EDF731D88C9AEAD52258B2E2C4740EF614F02E9D91E9B7B59622A3C'
15+
16+// ecdsa-sig-03 (P-521) — note: the file title says "P-512" but the
17+// IANA name is P-521 (521-bit curve, hence 66-byte coordinates).
18+const p521_x_b64u = 'AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt'
19+const p521_y_b64u = 'AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1'
20+const p521_d_b64u = 'AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt'
21+const p521_sig03_message = 'D28444A1013823A104581E62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C6554546869732069732074686520636F6E74656E742E588401664DD6962091B5100D6E1833D503539330EC2BC8FD3E8996950CE9F70259D9A30F73794F603B0D3E7C5E9C4C2A57E10211F76E79DF8FFD1B79D7EF5B9FA7DA109001965FA2D37E093BB13C040399C467B3B9908C09DB2B0F1F4996FE07BB02AAA121A8E1C671F3F997ADE7D651081017057BD3A8A5FBF394972EA71CFDC15E6F8FE2E1'
22+
23+const sample_text = 'This is the content.'
24+
25+fn test_verify1_accepts_p384_reference_vector() {
26+ x := base64.url_decode(p384_x_b64u)
27+ y := base64.url_decode(p384_y_b64u)
28+ pub_key := Key.ec2_public(.p_384, x, y)
29+ msg := hex.decode(p384_sig02_message)!
30+ payload := verify1(msg, pub_key)!
31+ assert payload == sample_text.bytes()
32+}
33+
34+fn test_sign1_p384_roundtrip() {
35+ x := base64.url_decode(p384_x_b64u)
36+ y := base64.url_decode(p384_y_b64u)
37+ d := base64.url_decode(p384_d_b64u)
38+ priv_key := Key.ec2_private(.p_384, x, y, d)
39+ pub_key := Key.ec2_public(.p_384, x, y)
40+ mut hp := Headers{}
41+ hp.algorithm = .es384
42+ signed := sign1('p384'.bytes(), priv_key, protected: hp)!
43+ got := verify1(signed, pub_key)!
44+ assert got == 'p384'.bytes()
45+}
46+
47+fn test_verify1_accepts_p521_reference_vector() {
48+ x := base64.url_decode(p521_x_b64u)
49+ y := base64.url_decode(p521_y_b64u)
50+ pub_key := Key.ec2_public(.p_521, x, y)
51+ msg := hex.decode(p521_sig03_message)!
52+ payload := verify1(msg, pub_key)!
53+ assert payload == sample_text.bytes()
54+}
55+
56+fn test_sign1_p521_roundtrip() {
57+ x := base64.url_decode(p521_x_b64u)
58+ y := base64.url_decode(p521_y_b64u)
59+ d := base64.url_decode(p521_d_b64u)
60+ priv_key := Key.ec2_private(.p_521, x, y, d)
61+ pub_key := Key.ec2_public(.p_521, x, y)
62+ mut hp := Headers{}
63+ hp.algorithm = .es512
64+ signed := sign1('p521'.bytes(), priv_key, protected: hp)!
65+ got := verify1(signed, pub_key)!
66+ assert got == 'p521'.bytes()
67+}
@@ -0,0 +1,231 @@
1+// COSE_Sign — multi-signer signed message — RFC 9052 §4.1.
2+//
3+// COSE_Sign carries one payload signed by N signers, each with its own
4+// per-signer protected/unprotected headers and signature. Each signer
5+// can use a different algorithm. Verification is per-signer: the body
6+// is considered authentic if at least one signature verifies (the
7+// caller decides which signers it trusts).
8+module cose
9+
10+import encoding.cbor
11+
12+// max_signers caps the `signatures` array size a decoder will accept.
13+// RFC 9052 places no explicit bound, but real-world COSE_Sign messages
14+// carry one or a handful of signers; anything wildly larger is almost
15+// certainly a malformed or hostile input trying to OOM the parser.
16+const max_signers = 256
17+
18+// Signature is one entry of the `signatures` array of a COSE_Sign
19+// message. The per-signer protected header MUST contain the algorithm.
20+pub struct Signature {
21+pub mut:
22+ protected Headers
23+ unprotected Headers
24+ signature []u8
25+}
26+
27+// SignMessage is the V representation of a COSE_Sign message.
28+pub struct SignMessage {
29+pub mut:
30+ protected Headers
31+ unprotected Headers
32+ payload ?[]u8
33+ signatures []Signature
34+}
35+
36+// Signer bundles a signing key with the per-signer headers that go
37+// into the COSE_Sign message. Use a separate `Signer` per identity
38+// when producing a multi-signer message.
39+pub struct Signer {
40+pub:
41+ key Key
42+ protected Headers
43+ unprotected Headers
44+}
45+
46+// SignOptions bundles inputs to `cose.sign`.
47+@[params]
48+pub struct SignOptions {
49+pub:
50+ protected Headers
51+ unprotected Headers
52+ external_aad []u8
53+ detached_payload ?[]u8
54+ untagged bool
55+}
56+
57+// sign produces a tagged COSE_Sign message in one call. Each `Signer`
58+// in `signers` adds one entry to the `signatures` array; their
59+// per-signer protected headers must include the algorithm to use.
60+pub fn sign(payload []u8, signers []Signer, opts SignOptions) ![]u8 {
61+ if signers.len == 0 {
62+ return error('cose: COSE_Sign requires at least one signer')
63+ }
64+ signed_bytes := opts.detached_payload or { payload }
65+ body_protected := opts.protected.encode_protected()!
66+
67+ mut entries := []Signature{cap: signers.len}
68+ for s in signers {
69+ alg := s.protected.algorithm or {
70+ return error('cose: each signer must declare an algorithm in protected headers')
71+ }
72+
73+ sign_protected := s.protected.encode_protected()!
74+ tbs := sig_structure_sign(body_protected, sign_protected, opts.external_aad, signed_bytes)
75+ sig := sign_with_key(alg, s.key, tbs)!
76+ entries << Signature{
77+ protected: s.protected
78+ unprotected: s.unprotected
79+ signature: sig
80+ }
81+ }
82+
83+ mut msg := SignMessage{
84+ protected: opts.protected
85+ unprotected: opts.unprotected
86+ payload: payload
87+ signatures: entries
88+ }
89+ if opts.detached_payload != none {
90+ msg.payload = none
91+ }
92+ return msg.encode(!opts.untagged)!
93+}
94+
95+// VerifySignOptions bundles inputs to per-signer verification.
96+@[params]
97+pub struct VerifySignOptions {
98+pub:
99+ external_aad []u8
100+ detached_payload ?[]u8
101+}
102+
103+// verify checks the signature at `signer_index` of the message against
104+// `key`. By default the payload is taken from `m.payload`; pass
105+// `opts.detached_payload` for the detached case. The per-signer
106+// algorithm MUST be in that signer's protected header (RFC 9052 §3).
107+pub fn (m SignMessage) verify(signer_index int, key Key, opts VerifySignOptions) ! {
108+ if signer_index < 0 || signer_index >= m.signatures.len {
109+ return error('cose: signer index ${signer_index} out of range (have ${m.signatures.len})')
110+ }
111+ entry := m.signatures[signer_index]
112+ check_protected_headers(m.protected, m.unprotected)!
113+ check_protected_headers(entry.protected, entry.unprotected)!
114+ alg := entry.protected.algorithm or {
115+ return MalformedMessage{
116+ reason: 'signer at index ${signer_index} missing algorithm in protected header'
117+ }
118+ }
119+
120+ pl := if dp := opts.detached_payload {
121+ dp
122+ } else {
123+ m.payload or {
124+ return MalformedMessage{
125+ reason: 'detached payload requires VerifySignOptions.detached_payload'
126+ }
127+ }
128+ }
129+ body_protected := m.protected.encode_protected()!
130+ sign_protected := entry.protected.encode_protected()!
131+ tbs := sig_structure_sign(body_protected, sign_protected, opts.external_aad, pl)
132+ verify_with_key(alg, key, tbs, entry.signature)!
133+}
134+
135+// encode serialises the SignMessage. When `tagged` is true the output
136+// is wrapped in CBOR tag 98.
137+pub fn (m SignMessage) encode(tagged bool) ![]u8 {
138+ body_protected := m.protected.encode_protected()!
139+
140+ mut p := cbor.new_packer(cbor.EncodeOpts{ canonical: true })
141+ if tagged {
142+ p.pack_tag(tag_sign)
143+ }
144+ p.pack_array_header(4)
145+ p.pack_bytes(body_protected)
146+ p.pack_value(m.unprotected.to_value())!
147+ if pl := m.payload {
148+ p.pack_bytes(pl)
149+ } else {
150+ p.pack_null()
151+ }
152+ p.pack_array_header(u64(m.signatures.len))
153+ for entry in m.signatures {
154+ sp := entry.protected.encode_protected()!
155+ p.pack_array_header(3)
156+ p.pack_bytes(sp)
157+ p.pack_value(entry.unprotected.to_value())!
158+ p.pack_bytes(entry.signature)
159+ }
160+ return p.bytes()
161+}
162+
163+// SignMessage.decode parses a CBOR-encoded COSE_Sign.
164+pub fn SignMessage.decode(data []u8) !SignMessage {
165+ mut u := cbor.new_unpacker(data, cbor.DecodeOpts{})
166+ if u.peek_kind()! == .tag_val {
167+ tag_no := u.unpack_tag()!
168+ if tag_no != tag_sign {
169+ return MalformedMessage{
170+ reason: 'expected tag ${tag_sign} (Sign), got ${tag_no}'
171+ }
172+ }
173+ }
174+ header_count := u.unpack_array_header()!
175+ if header_count != 4 {
176+ return MalformedMessage{
177+ reason: 'Sign array must have 4 elements, got ${header_count}'
178+ }
179+ }
180+
181+ protected := parse_protected(u.unpack_bytes()!)!
182+ unprotected := parse_headers_value(u.unpack_value()!)!
183+ mut payload := ?[]u8(none)
184+ if u.peek_kind()! == .null_val {
185+ u.unpack_null()!
186+ } else {
187+ payload = u.unpack_bytes()!
188+ }
189+
190+ signers_count := u.unpack_array_header()!
191+ if signers_count <= 0 {
192+ return MalformedMessage{
193+ reason: 'Sign requires at least one signature, got ${signers_count}'
194+ }
195+ }
196+ if signers_count > max_signers {
197+ return MalformedMessage{
198+ reason: 'Sign claims ${signers_count} signatures (over ${max_signers}-entry sanity cap)'
199+ }
200+ }
201+ mut signatures := []Signature{cap: int(signers_count)}
202+ for _ in 0 .. signers_count {
203+ if u.unpack_array_header()! != 3 {
204+ return MalformedMessage{
205+ reason: 'COSE_Signature array must have 3 elements'
206+ }
207+ }
208+ // Bind the three fields to locals so the read order from the
209+ // unpacker stays explicit (struct-literal evaluation order is
210+ // source order in V today, but we don't want to rely on that).
211+ s_protected := parse_protected(u.unpack_bytes()!)!
212+ s_unprotected := parse_headers_value(u.unpack_value()!)!
213+ sig := u.unpack_bytes()!
214+ signatures << Signature{
215+ protected: s_protected
216+ unprotected: s_unprotected
217+ signature: sig
218+ }
219+ }
220+ if !u.done() {
221+ return MalformedMessage{
222+ reason: 'trailing bytes after Sign'
223+ }
224+ }
225+ return SignMessage{
226+ protected: protected
227+ unprotected: unprotected
228+ payload: payload
229+ signatures: signatures
230+ }
231+}
@@ -0,0 +1,184 @@
1+// COSE_Sign1 — single-signer signed message — RFC 9052 §4.2.
2+module cose
3+
4+import encoding.cbor
5+
6+// Sign1Message is the V representation of a COSE_Sign1 message. The
7+// fields map 1:1 to the CBOR array slots defined by RFC 9052 §4.2.
8+//
9+// Use the `cose.sign1(...)` shorthand for the common case (sign +
10+// encode in one step) and the type's methods (`encode`, `sign`,
11+// `verify`) when finer control over the message is needed (e.g. setting
12+// custom headers, doing a detached payload, or signing with an external
13+// AAD).
14+pub struct Sign1Message {
15+pub mut:
16+ protected Headers
17+ unprotected Headers
18+ // payload — `none` means a detached payload (the actual payload is
19+ // transmitted out of band). The signature is still computed over the
20+ // real payload bytes; supply them via `Sign1Options.detached_payload`
21+ // when signing or via `Verify1Options.detached_payload` when
22+ // verifying.
23+ payload ?[]u8
24+ signature []u8
25+}
26+
27+// Sign1Options bundles the parameters that drive `cose.sign1`. Only the
28+// algorithm in `protected.algorithm` is mandatory; everything else has
29+// safe defaults.
30+@[params]
31+pub struct Sign1Options {
32+pub:
33+ protected Headers
34+ unprotected Headers
35+ // external_aad is data that is included in the signature
36+ // computation but not transmitted in the message. Both signer and
37+ // verifier must supply the same value.
38+ external_aad []u8
39+ // detached_payload, if set, overrides `payload` for the purpose of
40+ // the signature input. When detached, the encoded message will
41+ // contain a CBOR `nil` in the payload slot rather than the bytes.
42+ detached_payload ?[]u8
43+ // untagged emits the message without the surrounding tag 18 wrapper.
44+ untagged bool
45+}
46+
47+// Verify1Options bundles inputs to `cose.verify1`. Defaults are the
48+// usual case (tagged message, attached payload, no external AAD).
49+@[params]
50+pub struct Verify1Options {
51+pub:
52+ external_aad []u8
53+ detached_payload ?[]u8
54+}
55+
56+// sign1 produces a tagged COSE_Sign1 message in one call. The
57+// algorithm in `opts.protected.algorithm` selects the signing routine
58+// and is integrity-protected by the signature.
59+pub fn sign1(payload []u8, key Key, opts Sign1Options) ![]u8 {
60+ signed_bytes := opts.detached_payload or { payload }
61+ mut msg := Sign1Message{
62+ protected: opts.protected
63+ unprotected: opts.unprotected
64+ payload: payload
65+ }
66+ msg.sign(key, signed_bytes, opts.external_aad)!
67+ if opts.detached_payload != none {
68+ msg.payload = none
69+ }
70+ return msg.encode(!opts.untagged)!
71+}
72+
73+// verify1 parses a (tagged or untagged) COSE_Sign1, verifies the
74+// signature against `key`, and returns the payload. For detached
75+// payloads, the caller passes the bytes via `opts.detached_payload`.
76+pub fn verify1(message []u8, key Key, opts Verify1Options) ![]u8 {
77+ msg := Sign1Message.decode(message)!
78+ pl := if dp := opts.detached_payload {
79+ dp
80+ } else {
81+ msg.payload or {
82+ return MalformedMessage{
83+ reason: 'detached payload requires Verify1Options.detached_payload'
84+ }
85+ }
86+ }
87+ msg.verify(key, pl, opts.external_aad)!
88+ return pl
89+}
90+
91+// sign computes the signature over the Sig_structure built from the
92+// message's protected headers and the supplied payload. The result is
93+// stored in `signature`. The message itself isn't mutated outside of
94+// the signature; the caller is expected to have set the algorithm in
95+// `protected` before calling.
96+pub fn (mut m Sign1Message) sign(key Key, payload []u8, external_aad []u8) ! {
97+ alg := m.protected.algorithm or {
98+ return error('cose: Sign1Message.sign requires protected.algorithm to be set')
99+ }
100+
101+ body_protected := m.protected.encode_protected()!
102+ tbs := sig_structure_sign1(body_protected, external_aad, payload)
103+ m.signature = sign_with_key(alg, key, tbs)!
104+}
105+
106+// verify recomputes the Sig_structure from the message's protected
107+// headers and the supplied payload, then checks the signature. The
108+// algorithm MUST be present in the protected headers per RFC 9052 §3
109+// — putting it in `unprotected` would let an attacker substitute it
110+// without invalidating the signature.
111+pub fn (m Sign1Message) verify(key Key, payload []u8, external_aad []u8) ! {
112+ check_protected_headers(m.protected, m.unprotected)!
113+ alg := m.protected.algorithm or {
114+ return MalformedMessage{
115+ reason: 'algorithm missing from protected header (RFC 9052 §3)'
116+ }
117+ }
118+
119+ body_protected := m.protected.encode_protected()!
120+ tbs := sig_structure_sign1(body_protected, external_aad, payload)
121+ verify_with_key(alg, key, tbs, m.signature)!
122+}
123+
124+// encode serialises the message. When `tagged` is true the output is
125+// wrapped in CBOR tag 18 (RFC 9052 §2).
126+pub fn (m Sign1Message) encode(tagged bool) ![]u8 {
127+ body_protected := m.protected.encode_protected()!
128+
129+ mut p := cbor.new_packer(cbor.EncodeOpts{ canonical: true })
130+ if tagged {
131+ p.pack_tag(tag_sign1)
132+ }
133+ p.pack_array_header(4)
134+ p.pack_bytes(body_protected)
135+ p.pack_value(m.unprotected.to_value())!
136+ if pl := m.payload {
137+ p.pack_bytes(pl)
138+ } else {
139+ p.pack_null()
140+ }
141+ p.pack_bytes(m.signature)
142+ return p.bytes()
143+}
144+
145+// Sign1Message.decode parses a CBOR-encoded COSE_Sign1. Both the tagged
146+// (tag 18) and untagged forms are accepted.
147+pub fn Sign1Message.decode(data []u8) !Sign1Message {
148+ mut u := cbor.new_unpacker(data, cbor.DecodeOpts{})
149+ if u.peek_kind()! == .tag_val {
150+ tag_no := u.unpack_tag()!
151+ if tag_no != tag_sign1 {
152+ return MalformedMessage{
153+ reason: 'expected tag ${tag_sign1} (Sign1), got ${tag_no}'
154+ }
155+ }
156+ }
157+ header_count := u.unpack_array_header()!
158+ if header_count != 4 {
159+ return MalformedMessage{
160+ reason: 'Sign1 array must have 4 elements, got ${header_count}'
161+ }
162+ }
163+
164+ protected := parse_protected(u.unpack_bytes()!)!
165+ unprotected := parse_headers_value(u.unpack_value()!)!
166+ mut payload := ?[]u8(none)
167+ if u.peek_kind()! == .null_val {
168+ u.unpack_null()!
169+ } else {
170+ payload = u.unpack_bytes()!
171+ }
172+ signature := u.unpack_bytes()!
173+ if !u.done() {
174+ return MalformedMessage{
175+ reason: 'trailing bytes after Sign1'
176+ }
177+ }
178+ return Sign1Message{
179+ protected: protected
180+ unprotected: unprotected
181+ payload: payload
182+ signature: signature
183+ }
184+}
@@ -0,0 +1,239 @@
1+// Tests for COSE_Sign1: roundtrip per algorithm and bytes-exact match
2+// against cose-wg/Examples reference vectors. EdDSA signatures are
3+// deterministic (RFC 8032 §5.1.6) so we can compare bytes directly;
4+// ECDSA signatures use a fresh nonce each run, so for ES* we test by
5+// verifying the message produced by another implementation and by
6+// round-tripping our own output through verify1.
7+module cose
8+
9+import encoding.base64
10+import encoding.hex
11+
12+// EdDSA ed25519-sig-01 from cose-wg/Examples (Unlicense).
13+const eddsa_d_hex = '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60'
14+const eddsa_x_hex = 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
15+const eddsa_sig01_message = 'D28445A201270300A10442313154546869732069732074686520636F6E74656E742E58407142FD2FF96D56DB85BEE905A76BA1D0B7321A95C8C4D3607C5781932B7AFB8711497DFA751BF40B58B3BCC32300B1487F3DB34085EEF013BF08F4A44D6FEF0D'
16+
17+// ECDSA ecdsa-sig-01 from cose-wg/Examples.
18+const ecdsa_p256_x_b64u = 'usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8'
19+const ecdsa_p256_y_b64u = 'IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4'
20+const ecdsa_p256_d_b64u = 'V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM'
21+const ecdsa_sig01_message = 'D28445A201260300A10442313154546869732069732074686520636F6E74656E742E58406520BBAF2081D7E0ED0F95F76EB0733D667005F7467CEC4B87B9381A6BA1EDE8E00DF29F32A37230F39A842A54821FDD223092819D7728EFB9D3A0080B75380B'
22+
23+const sample_text = 'This is the content.'
24+
25+fn test_sign1_eddsa_matches_reference_vector() {
26+ // EdDSA is deterministic, so we can match the reference bytes-exact.
27+ d := hex.decode(eddsa_d_hex)!
28+ x := hex.decode(eddsa_x_hex)!
29+ key := Key.okp_private(.ed25519, x, d)
30+ mut hp := Headers{}
31+ hp.algorithm = .eddsa
32+ hp.content_type_int = u64(0)
33+ mut hu := Headers{}
34+ hu.kid = '11'.bytes()
35+ got := sign1(sample_text.bytes(), key, protected: hp, unprotected: hu)!
36+ want := hex.decode(eddsa_sig01_message)!
37+ assert got == want
38+}
39+
40+fn test_verify1_accepts_reference_eddsa_message() {
41+ x := hex.decode(eddsa_x_hex)!
42+ pub_key := Key.okp_public(.ed25519, x)
43+ msg := hex.decode(eddsa_sig01_message)!
44+ payload := verify1(msg, pub_key)!
45+ assert payload == sample_text.bytes()
46+}
47+
48+fn test_verify1_accepts_reference_ecdsa_p256_message() {
49+ x := base64.url_decode(ecdsa_p256_x_b64u)
50+ y := base64.url_decode(ecdsa_p256_y_b64u)
51+ pub_key := Key.ec2_public(.p_256, x, y)
52+ msg := hex.decode(ecdsa_sig01_message)!
53+ payload := verify1(msg, pub_key)!
54+ assert payload == sample_text.bytes()
55+}
56+
57+fn test_sign1_ecdsa_p256_roundtrip() {
58+ x := base64.url_decode(ecdsa_p256_x_b64u)
59+ y := base64.url_decode(ecdsa_p256_y_b64u)
60+ d := base64.url_decode(ecdsa_p256_d_b64u)
61+ priv_key := Key.ec2_private(.p_256, x, y, d)
62+ pub_key := Key.ec2_public(.p_256, x, y)
63+ mut hp := Headers{}
64+ hp.algorithm = .es256
65+ mut hu := Headers{}
66+ hu.kid = '11'.bytes()
67+ signed := sign1('hello'.bytes(), priv_key, protected: hp, unprotected: hu)!
68+ got := verify1(signed, pub_key)!
69+ assert got == 'hello'.bytes()
70+}
71+
72+fn test_verify1_rejects_tampered_payload() {
73+ x := hex.decode(eddsa_x_hex)!
74+ pub_key := Key.okp_public(.ed25519, x)
75+ mut msg := hex.decode(eddsa_sig01_message)!
76+ // Find the last byte of the payload bstr and flip a bit. The payload
77+ // "This is the content." (20 bytes) starts after the unprotected
78+ // map. We flip the first content byte by scanning for the bstr
79+ // header 0x54 (bstr of length 20).
80+ mut idx := 0
81+ for idx < msg.len {
82+ if msg[idx] == 0x54 && idx + 20 < msg.len {
83+ break
84+ }
85+ idx++
86+ }
87+ msg[idx + 1] ^= 0x01
88+ if _ := verify1(msg, pub_key) {
89+ assert false, 'tampered payload must not verify'
90+ } else {
91+ assert err is VerificationFailed
92+ }
93+}
94+
95+fn test_verify1_rejects_wrong_key() {
96+ // Use a different Ed25519 public key (zeros) to verify the reference
97+ // message — it must fail.
98+ wrong_x := []u8{len: 32, init: 0}
99+ pub_key := Key.okp_public(.ed25519, wrong_x)
100+ msg := hex.decode(eddsa_sig01_message)!
101+ if _ := verify1(msg, pub_key) {
102+ assert false, 'wrong key must not verify'
103+ } else {
104+ assert err is VerificationFailed
105+ }
106+}
107+
108+fn test_verify1_rejects_unknown_critical_label() {
109+ // Per RFC 9052 §3.1, a verifier MUST fail when `crit` lists a
110+ // label it does not understand. Build a message whose protected
111+ // header carries a crit list referencing label 99 (unknown).
112+ x := hex.decode(eddsa_x_hex)!
113+ d := hex.decode(eddsa_d_hex)!
114+ priv_key := Key.okp_private(.ed25519, x, d)
115+ pub_key := Key.okp_public(.ed25519, x)
116+ mut hp := Headers{}
117+ hp.algorithm = .eddsa
118+ hp.critical = [i64(99)]
119+ signed := sign1('payload'.bytes(), priv_key, protected: hp)!
120+ if _ := verify1(signed, pub_key) {
121+ assert false, 'must reject unknown crit label'
122+ } else {
123+ assert err is MalformedMessage
124+ assert err.msg().contains('crit lists unknown label')
125+ }
126+}
127+
128+fn test_verify1_accepts_known_critical_label() {
129+ // `crit` listing only known labels (e.g. label 1 = alg) must
130+ // not block verification.
131+ x := hex.decode(eddsa_x_hex)!
132+ d := hex.decode(eddsa_d_hex)!
133+ priv_key := Key.okp_private(.ed25519, x, d)
134+ pub_key := Key.okp_public(.ed25519, x)
135+ mut hp := Headers{}
136+ hp.algorithm = .eddsa
137+ hp.critical = [i64(1)]
138+ signed := sign1('payload'.bytes(), priv_key, protected: hp)!
139+ got := verify1(signed, pub_key)!
140+ assert got == 'payload'.bytes()
141+}
142+
143+fn test_verify1_rejects_critical_in_unprotected_header() {
144+ x := hex.decode(eddsa_x_hex)!
145+ d := hex.decode(eddsa_d_hex)!
146+ priv_key := Key.okp_private(.ed25519, x, d)
147+ pub_key := Key.okp_public(.ed25519, x)
148+ mut hp := Headers{}
149+ hp.algorithm = .eddsa
150+ mut hu := Headers{}
151+ hu.critical = [i64(1)]
152+ signed := sign1('payload'.bytes(), priv_key, protected: hp, unprotected: hu)!
153+ if _ := verify1(signed, pub_key) {
154+ assert false, 'crit in unprotected headers must be rejected'
155+ } else {
156+ assert err is MalformedMessage
157+ assert err.msg().contains('crit label must be in protected headers')
158+ }
159+}
160+
161+fn test_verify1_rejects_critical_label_missing_from_protected_header() {
162+ x := hex.decode(eddsa_x_hex)!
163+ d := hex.decode(eddsa_d_hex)!
164+ priv_key := Key.okp_private(.ed25519, x, d)
165+ pub_key := Key.okp_public(.ed25519, x)
166+ mut hp := Headers{}
167+ hp.algorithm = .eddsa
168+ hp.critical = [i64(4)]
169+ mut hu := Headers{}
170+ hu.kid = 'kid-1'.bytes()
171+ signed := sign1('payload'.bytes(), priv_key, protected: hp, unprotected: hu)!
172+ if _ := verify1(signed, pub_key) {
173+ assert false, 'crit labels must be present in protected headers'
174+ } else {
175+ assert err is MalformedMessage
176+ assert err.msg().contains('not present in protected headers')
177+ }
178+}
179+
180+fn test_sign1_rejects_key_alg_mismatch() {
181+ // A key declaring `alg = ES256` must not be silently used for an
182+ // EdDSA signing call: this catches a common copy-paste mistake.
183+ x := hex.decode(eddsa_x_hex)!
184+ d := hex.decode(eddsa_d_hex)!
185+ mut key := Key.okp_private(.ed25519, x, d)
186+ key.alg = .es256 // wrong intent for an OKP key
187+ mut hp := Headers{}
188+ hp.algorithm = .eddsa
189+ if _ := sign1('payload'.bytes(), key, protected: hp) {
190+ assert false, 'must reject alg mismatch'
191+ } else {
192+ assert err is AlgorithmMismatch
193+ }
194+}
195+
196+fn test_sign1_external_aad_changes_signature() {
197+ x := hex.decode(eddsa_x_hex)!
198+ d := hex.decode(eddsa_d_hex)!
199+ key := Key.okp_private(.ed25519, x, d)
200+ mut hp := Headers{}
201+ hp.algorithm = .eddsa
202+ a := sign1('payload'.bytes(), key, protected: hp)!
203+ b := sign1('payload'.bytes(), key, protected: hp, external_aad: 'context'.bytes())!
204+ assert a != b
205+}
206+
207+fn test_sign1_detached_payload_omits_payload_in_message() {
208+ x := hex.decode(eddsa_x_hex)!
209+ d := hex.decode(eddsa_d_hex)!
210+ priv_key := Key.okp_private(.ed25519, x, d)
211+ pub_key := Key.okp_public(.ed25519, x)
212+ mut hp := Headers{}
213+ hp.algorithm = .eddsa
214+ signed := sign1([]u8{}, priv_key,
215+ protected: hp
216+ detached_payload: 'remote payload'.bytes()
217+ )!
218+ // The encoded message must contain a CBOR null where the payload
219+ // would normally be. Decode and check.
220+ msg := Sign1Message.decode(signed)!
221+ assert msg.payload == none
222+ // Verifier needs the detached bytes back.
223+ got := verify1(signed, pub_key, detached_payload: 'remote payload'.bytes())!
224+ assert got == 'remote payload'.bytes()
225+}
226+
227+fn test_sign1_untagged_roundtrip() {
228+ x := hex.decode(eddsa_x_hex)!
229+ d := hex.decode(eddsa_d_hex)!
230+ priv_key := Key.okp_private(.ed25519, x, d)
231+ pub_key := Key.okp_public(.ed25519, x)
232+ mut hp := Headers{}
233+ hp.algorithm = .eddsa
234+ signed := sign1('payload'.bytes(), priv_key, protected: hp, untagged: true)!
235+ // First byte must be 0x84 (array(4)), not 0xD2 (tag 18).
236+ assert signed[0] == 0x84
237+ got := verify1(signed, pub_key)!
238+ assert got == 'payload'.bytes()
239+}
@@ -0,0 +1,110 @@
1+// Tests for COSE_Sign — multi-signer signed messages.
2+module cose
3+
4+import encoding.base64
5+import encoding.hex
6+
7+const sign_p256_x = 'usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8'
8+const sign_p256_y = 'IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4'
9+const sign_p256_d = 'V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM'
10+
11+const sign_eddsa_x = 'd75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'
12+const sign_eddsa_d = '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60'
13+
14+fn test_sign_two_signers_roundtrip() {
15+ // One ES256 signer + one EdDSA signer over the same payload.
16+ x_ec := base64.url_decode(sign_p256_x)
17+ y_ec := base64.url_decode(sign_p256_y)
18+ d_ec := base64.url_decode(sign_p256_d)
19+ x_ed := hex.decode(sign_eddsa_x)!
20+ d_ed := hex.decode(sign_eddsa_d)!
21+
22+ mut hp_ec := Headers{}
23+ hp_ec.algorithm = .es256
24+ mut hu_ec := Headers{}
25+ hu_ec.kid = '11'.bytes()
26+ signer_ec := Signer{
27+ key: Key.ec2_private(.p_256, x_ec, y_ec, d_ec)
28+ protected: hp_ec
29+ unprotected: hu_ec
30+ }
31+
32+ mut hp_ed := Headers{}
33+ hp_ed.algorithm = .eddsa
34+ mut hu_ed := Headers{}
35+ hu_ed.kid = 'ed'.bytes()
36+ signer_ed := Signer{
37+ key: Key.okp_private(.ed25519, x_ed, d_ed)
38+ protected: hp_ed
39+ unprotected: hu_ed
40+ }
41+
42+ signed := sign('multi-signed payload'.bytes(), [signer_ec, signer_ed])!
43+ msg := SignMessage.decode(signed)!
44+ assert msg.signatures.len == 2
45+ assert (msg.payload or { []u8{} }) == 'multi-signed payload'.bytes()
46+
47+ // Verify the EC signer
48+ pub_ec := Key.ec2_public(.p_256, x_ec, y_ec)
49+ msg.verify(0, pub_ec)!
50+ // Verify the Ed signer
51+ pub_ed := Key.okp_public(.ed25519, x_ed)
52+ msg.verify(1, pub_ed)!
53+}
54+
55+fn test_sign_rejects_signer_without_alg() {
56+ x := hex.decode(sign_eddsa_x)!
57+ d := hex.decode(sign_eddsa_d)!
58+ bad_signer := Signer{
59+ key: Key.okp_private(.ed25519, x, d)
60+ // no protected.algorithm
61+ }
62+ if _ := sign('payload'.bytes(), [bad_signer]) {
63+ assert false, 'must reject signer without algorithm'
64+ } else {
65+ assert err.msg().contains('must declare an algorithm')
66+ }
67+}
68+
69+fn test_sign_decode_rejects_huge_signers_count() {
70+ // Hand-built COSE_Sign with a signatures-array length declared as
71+ // 0x1A 0xFFFFFFFF (≈ 4 billion). Without a cap this would trigger
72+ // a multi-GB allocation. With the cap we should fail fast.
73+ mut bad := []u8{}
74+ bad << 0xD8 // tag
75+ bad << 0x62 // tag 98
76+ bad << 0x84 // array(4)
77+ bad << 0x40 // bstr(0)
78+ bad << 0xA0 // map(0)
79+ bad << 0x40 // bstr(0) — payload
80+ // signatures array header: 0x9A LEN_4_BYTES = array of length n
81+ bad << 0x9A
82+ bad << 0xFF
83+ bad << 0xFF
84+ bad << 0xFF
85+ bad << 0xFF
86+ if _ := SignMessage.decode(bad) {
87+ assert false, 'must reject huge signers count'
88+ } else {
89+ assert err is MalformedMessage
90+ }
91+}
92+
93+fn test_sign_verify_wrong_signer_fails() {
94+ x := hex.decode(sign_eddsa_x)!
95+ d := hex.decode(sign_eddsa_d)!
96+ mut hp := Headers{}
97+ hp.algorithm = .eddsa
98+ signer := Signer{
99+ key: Key.okp_private(.ed25519, x, d)
100+ protected: hp
101+ }
102+ signed := sign('payload'.bytes(), [signer])!
103+ msg := SignMessage.decode(signed)!
104+ wrong := Key.okp_public(.ed25519, []u8{len: 32, init: 0})
105+ if _ := msg.verify(0, wrong) {
106+ assert false, 'must not verify with wrong key'
107+ } else {
108+ assert err is VerificationFailed
109+ }
110+}
@@ -0,0 +1,272 @@
1+// Algorithm-aware signing and verification helpers. These are the only
2+// places in the module that touch `crypto.ecdsa` and `crypto.ed25519`,
3+// so the signature/verification paths for new algorithms can be added
4+// here without rippling through the message types.
5+module cose
6+
7+import crypto.ecdsa
8+import crypto.ed25519
9+
10+// EcParams bundles the parameters that change between ES256/384/512:
11+// expected curve, OpenSSL NID, and coordinate byte width.
12+struct EcParams {
13+ curve Curve
14+ nid ecdsa.Nid
15+ coord_size int
16+}
17+
18+// ec_params_for returns the curve / NID / coordinate size for an ECDSA
19+// COSE algorithm. Errors out for non-ECDSA algorithms.
20+fn ec_params_for(alg Algorithm) !EcParams {
21+ return match alg {
22+ .es256 {
23+ EcParams{
24+ curve: .p_256
25+ nid: .prime256v1
26+ coord_size: 32
27+ }
28+ }
29+ .es384 {
30+ EcParams{
31+ curve: .p_384
32+ nid: .secp384r1
33+ coord_size: 48
34+ }
35+ }
36+ .es512 {
37+ EcParams{
38+ curve: .p_521
39+ nid: .secp521r1
40+ coord_size: 66 // P-521 → ⌈521/8⌉ = 66
41+ }
42+ }
43+ else {
44+ error('cose: not an ECDSA algorithm: ${alg.name()}')
45+ }
46+ }
47+}
48+
49+// check_ec_key validates that `key` is a usable EC2 key for `alg` and
50+// returns the matching EcParams. Used by both sign_with_key and
51+// verify_with_key to share the upfront validation.
52+fn check_ec_key(alg Algorithm, key Key) !EcParams {
53+ if key.kty != .ec2 {
54+ return error('cose: ${alg.name()} requires kty=EC2, got ${key.kty}')
55+ }
56+ params := ec_params_for(alg)!
57+ crv := key.crv or { return error('cose: EC2 key missing crv') }
58+ if crv != params.curve {
59+ return error('cose: ${alg.name()} requires crv=${params.curve}, got ${crv}')
60+ }
61+ return params
62+}
63+
64+// check_okp_key validates that `key` is a usable OKP/Ed25519 key.
65+fn check_okp_key(key Key) ! {
66+ if key.kty != .okp {
67+ return error('cose: EdDSA requires kty=OKP, got ${key.kty}')
68+ }
69+ crv := key.crv or { return error('cose: OKP key missing crv') }
70+ if crv != .ed25519 {
71+ return error('cose: EdDSA requires crv=Ed25519, got ${crv}')
72+ }
73+}
74+
75+// sign_with_key signs `to_be_signed` with `key`, producing a COSE-format
76+// signature (`R || S` for ECDSA, raw 64 bytes for Ed25519). The
77+// algorithm comes from `alg` rather than from the key so callers can
78+// reuse a key across multiple signing operations — but a key that
79+// declares its own `alg` MUST match: this catches accidental
80+// mismatches between key generation and signing intent.
81+fn sign_with_key(alg Algorithm, key Key, to_be_signed []u8) ![]u8 {
82+ if !alg.is_signature() {
83+ return UnsupportedAlgorithm{
84+ algorithm: alg
85+ context: 'signing'
86+ }
87+ }
88+ if key_alg := key.alg {
89+ if key_alg != alg {
90+ return AlgorithmMismatch{
91+ expected: key_alg
92+ got: alg
93+ }
94+ }
95+ }
96+ d := key.d or { return error('cose: signing requires a private key (missing d)') }
97+
98+ match alg {
99+ .es256, .es384, .es512 {
100+ params := check_ec_key(alg, key)!
101+ priv := ecdsa.new_key_from_seed(d, ecdsa.CurveOptions{
102+ nid: params.nid
103+ fixed_size: true
104+ })!
105+ defer {
106+ priv.free()
107+ }
108+ der := priv.sign(to_be_signed, ecdsa.SignerOpts{})!
109+ return der_to_raw(der, params.coord_size)!
110+ }
111+ .eddsa {
112+ check_okp_key(key)!
113+ x := key.x or { return error('cose: OKP key missing x (public key)') }
114+ if d.len != ed25519.seed_size {
115+ return error('cose: Ed25519 seed must be ${ed25519.seed_size} bytes, got ${d.len}')
116+ }
117+ if x.len != ed25519.public_key_size {
118+ return error('cose: Ed25519 public key must be ${ed25519.public_key_size} bytes, got ${x.len}')
119+ }
120+ // vlib/crypto/ed25519 expects the 64-byte (seed || public-key)
121+ // concatenation as PrivateKey.
122+ mut full := []u8{cap: ed25519.private_key_size}
123+ full << d
124+ full << x
125+ return ed25519.sign(full, to_be_signed)!
126+ }
127+ else {
128+ return UnsupportedAlgorithm{
129+ algorithm: alg
130+ context: 'signing'
131+ }
132+ }
133+ }
134+}
135+
136+// verify_with_key checks that `signature` is a valid signature over
137+// `to_be_signed` under the COSE algorithm `alg` and the given key.
138+// Returns a `VerificationFailed` error if the check fails.
139+fn verify_with_key(alg Algorithm, key Key, to_be_signed []u8, signature []u8) ! {
140+ if !alg.is_signature() {
141+ return UnsupportedAlgorithm{
142+ algorithm: alg
143+ context: 'signature verification'
144+ }
145+ }
146+ if key_alg := key.alg {
147+ if key_alg != alg {
148+ return AlgorithmMismatch{
149+ expected: key_alg
150+ got: alg
151+ }
152+ }
153+ }
154+ match alg {
155+ .es256, .es384, .es512 {
156+ params := check_ec_key(alg, key)!
157+ x := key.x or { return error('cose: EC2 key missing x') }
158+ y := key.y or { return error('cose: EC2 key missing y') }
159+ if signature.len != 2 * params.coord_size {
160+ return VerificationFailed{
161+ algorithm: alg
162+ }
163+ }
164+ der_sig := raw_to_der(signature, params.coord_size)!
165+ spki := build_ec_spki(params.curve, x, y)!
166+ pubkey := ecdsa.pubkey_from_bytes(spki)!
167+ defer {
168+ pubkey.free()
169+ }
170+ ok := pubkey.verify(to_be_signed, der_sig, ecdsa.SignerOpts{}) or {
171+ return VerificationFailed{
172+ algorithm: alg
173+ }
174+ }
175+ if !ok {
176+ return VerificationFailed{
177+ algorithm: alg
178+ }
179+ }
180+ }
181+ .eddsa {
182+ check_okp_key(key)!
183+ x := key.x or { return error('cose: OKP key missing x') }
184+ if x.len != ed25519.public_key_size {
185+ return error('cose: Ed25519 public key must be ${ed25519.public_key_size} bytes')
186+ }
187+ ok := ed25519.verify(x, to_be_signed, signature) or {
188+ return VerificationFailed{
189+ algorithm: alg
190+ }
191+ }
192+ if !ok {
193+ return VerificationFailed{
194+ algorithm: alg
195+ }
196+ }
197+ }
198+ else {
199+ return UnsupportedAlgorithm{
200+ algorithm: alg
201+ context: 'signature verification'
202+ }
203+ }
204+ }
205+}
206+
207+// build_ec_spki assembles a SubjectPublicKeyInfo DER blob (RFC 5480) for
208+// an uncompressed EC public point. This is the format consumed by
209+// `ecdsa.pubkey_from_bytes`. We do this in pure V to avoid having to
210+// add new C bindings just to load a public key from raw coordinates.
211+fn build_ec_spki(crv Curve, x []u8, y []u8) ![]u8 {
212+ // id-ecPublicKey OID = 1.2.840.10045.2.1
213+ id_ec_public_key := [u8(0x06), 0x07, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x02, 0x01]
214+ // Per-curve OID:
215+ curve_oid := match crv {
216+ .p_256 { [u8(0x06), 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07] }
217+ .p_384 { [u8(0x06), 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22] }
218+ .p_521 { [u8(0x06), 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23] }
219+ else { return error('cose: not an EC2 curve for SPKI: ${crv}') }
220+ }
221+
222+ coord_size := match crv {
223+ .p_256 { 32 }
224+ .p_384 { 48 }
225+ .p_521 { 66 }
226+ else { return error('cose: not an EC2 curve for SPKI: ${crv}') }
227+ }
228+
229+ if x.len > coord_size || y.len > coord_size {
230+ return MalformedMessage{
231+ reason: 'EC2 coordinates exceed curve size'
232+ }
233+ }
234+
235+ // Uncompressed point: 0x04 || X (left-padded) || Y (left-padded).
236+ mut point := []u8{len: 1 + 2 * coord_size}
237+ point[0] = 0x04
238+ x_off := 1 + (coord_size - x.len)
239+ for i in 0 .. x.len {
240+ point[x_off + i] = x[i]
241+ }
242+ y_off := 1 + coord_size + (coord_size - y.len)
243+ for i in 0 .. y.len {
244+ point[y_off + i] = y[i]
245+ }
246+
247+ // AlgorithmIdentifier ::= SEQUENCE { id_ec_public_key, curve_oid }
248+ mut alg_id_body := []u8{cap: id_ec_public_key.len + curve_oid.len}
249+ alg_id_body << id_ec_public_key
250+ alg_id_body << curve_oid
251+ mut alg_id := []u8{cap: 2 + alg_id_body.len}
252+ alg_id << 0x30
253+ alg_id << encode_der_length(alg_id_body.len)
254+ alg_id << alg_id_body
255+
256+ // BIT STRING: 0x03 LEN UNUSED_BITS || POINT
257+ mut bit_string := []u8{cap: 3 + point.len}
258+ bit_string << 0x03
259+ bit_string << encode_der_length(point.len + 1)
260+ bit_string << 0x00 // 0 unused bits in the trailing octet
261+ bit_string << point
262+
263+ // SubjectPublicKeyInfo ::= SEQUENCE { alg_id, bit_string }
264+ mut spki_body := []u8{cap: alg_id.len + bit_string.len}
265+ spki_body << alg_id
266+ spki_body << bit_string
267+ mut spki := []u8{cap: 2 + spki_body.len}
268+ spki << 0x30
269+ spki << encode_der_length(spki_body.len)
270+ spki << spki_body
271+ return spki
272+}
@@ -0,0 +1,86 @@
1+// Construction of the data items signed (Sig_structure) and MACed
2+// (MAC_structure) by COSE messages, per RFC 9052 §4.4 and §6.3.
3+// These auxiliary structures never appear on the wire — they exist only
4+// to feed deterministic bytes into the cryptographic primitive.
5+module cose
6+
7+import encoding.cbor
8+
9+// sig_structure_sign1 builds the byte stream signed by COSE_Sign1
10+// (RFC 9052 §4.4):
11+//
12+// Sig_structure1 = [
13+// context : "Signature1",
14+// body_protected : empty_or_serialized_map,
15+// external_aad : bstr,
16+// payload : bstr
17+// ]
18+//
19+// `body_protected` is the raw bytes of the protected header bstr (i.e.
20+// what `Headers.encode_protected` returns; not the surrounding bstr).
21+// `external_aad` is application-supplied additional data that is NOT
22+// transmitted in the message but participates in the signature.
23+fn sig_structure_sign1(body_protected []u8, external_aad []u8, payload []u8) []u8 {
24+ mut p := cbor.new_packer(cbor.EncodeOpts{})
25+ p.pack_array_header(4)
26+ p.pack_text('Signature1')
27+ p.pack_bytes(body_protected)
28+ p.pack_bytes(external_aad)
29+ p.pack_bytes(payload)
30+ return p.bytes()
31+}
32+
33+// sig_structure_sign builds the byte stream signed by one signer of a
34+// COSE_Sign message (RFC 9052 §4.4):
35+//
36+// Sig_structure = [
37+// context : "Signature",
38+// body_protected : empty_or_serialized_map,
39+// sign_protected : empty_or_serialized_map,
40+// external_aad : bstr,
41+// payload : bstr
42+// ]
43+//
44+// The `sign_protected` field is the per-signer protected headers
45+// (distinct from the body's own protected headers).
46+fn sig_structure_sign(body_protected []u8, sign_protected []u8, external_aad []u8, payload []u8) []u8 {
47+ mut p := cbor.new_packer(cbor.EncodeOpts{})
48+ p.pack_array_header(5)
49+ p.pack_text('Signature')
50+ p.pack_bytes(body_protected)
51+ p.pack_bytes(sign_protected)
52+ p.pack_bytes(external_aad)
53+ p.pack_bytes(payload)
54+ return p.bytes()
55+}
56+
57+// mac_structure_mac0 builds the byte stream MACed by COSE_Mac0
58+// (RFC 9052 §6.3):
59+//
60+// MAC_structure = [
61+// context : "MAC0",
62+// protected : empty_or_serialized_map,
63+// external_aad : bstr,
64+// payload : bstr
65+// ]
66+fn mac_structure_mac0(protected []u8, external_aad []u8, payload []u8) []u8 {
67+ mut p := cbor.new_packer(cbor.EncodeOpts{})
68+ p.pack_array_header(4)
69+ p.pack_text('MAC0')
70+ p.pack_bytes(protected)
71+ p.pack_bytes(external_aad)
72+ p.pack_bytes(payload)
73+ return p.bytes()
74+}
75+
76+// mac_structure_mac is the analogue of mac_structure_mac0 for COSE_Mac
77+// (multi-recipient). The `context` differs ("MAC" instead of "MAC0").
78+fn mac_structure_mac(protected []u8, external_aad []u8, payload []u8) []u8 {
79+ mut p := cbor.new_packer(cbor.EncodeOpts{})
80+ p.pack_array_header(4)
81+ p.pack_text('MAC')
82+ p.pack_bytes(protected)
83+ p.pack_bytes(external_aad)
84+ p.pack_bytes(payload)
85+ return p.bytes()
86+}
@@ -0,0 +1,77 @@
1+// Tests for Sig_structure / MAC_structure construction. Reference values
2+// come from the cose-wg/Examples repository (Unlicense / public domain).
3+module cose
4+
5+import encoding.hex
6+
7+const sample_payload = 'This is the content.'.bytes()
8+
9+// From ECDSA-01.json (ES256 sign1):
10+// protected = bstr-wrapped CBOR map { alg: -7, ctyp: 0 }
11+// external_aad = empty
12+// payload = "This is the content."
13+// ToBeSign_hex = 846A5369676E61747572653145A2012603004054...
14+fn test_sig_structure_sign1_matches_ecdsa01_vector() {
15+ body_protected := hex.decode('A201260300')!
16+ want :=
17+ hex.decode('846A5369676E61747572653145A2012603004054546869732069732074686520636F6E74656E742E')!
18+ got := sig_structure_sign1(body_protected, []u8{}, sample_payload)
19+ assert got == want
20+}
21+
22+// From EdDSA-01.json (sign1):
23+// protected = bstr-wrapped CBOR map { alg: -8, ctyp: 0 }
24+// ToBeSign_hex = 846A5369676E61747572653145A2012703004054...
25+fn test_sig_structure_sign1_matches_eddsa01_vector() {
26+ body_protected := hex.decode('A201270300')!
27+ want :=
28+ hex.decode('846A5369676E61747572653145A2012703004054546869732069732074686520636F6E74656E742E')!
29+ got := sig_structure_sign1(body_protected, []u8{}, sample_payload)
30+ assert got == want
31+}
32+
33+// From CWT/A_3.json (RFC 8392 Appendix A.3 — Signed CWT w/ ES256):
34+// protected = bstr-wrapped { alg: -7 }
35+// payload = the encoded CWT claims set
36+fn test_sig_structure_sign1_matches_cwt_a3_vector() {
37+ body_protected := hex.decode('A10126')!
38+ cwt_claims :=
39+ hex.decode('a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b71')!
40+ want :=
41+ hex.decode('846A5369676E61747572653143A10126405850A70175636F61703A2F2F61732E6578616D706C652E636F6D02656572696B77037818636F61703A2F2F6C696768742E6578616D706C652E636F6D041A5612AEB0051A5610D9F0061A5610D9F007420B71')!
42+ got := sig_structure_sign1(body_protected, []u8{}, cwt_claims)
43+ assert got == want
44+}
45+
46+// Sig_structure for COSE_Sign uses the "Signature" (no "1") tag and
47+// adds a sign_protected slot.
48+fn test_sig_structure_sign_includes_sign_protected() {
49+ body_protected := []u8{} // empty body protected
50+ sign_protected := hex.decode('A10126')! // {alg:-7}
51+ got := sig_structure_sign(body_protected, sign_protected, []u8{}, sample_payload)
52+ // 85 array(5) | 69 "Signature" (text 9) | 40 (empty bstr) |
53+ // 43 A10126 (3-byte bstr) | 40 (empty external) |
54+ // 54 (payload bstr 20) | "This is..."
55+ assert got[0] == 0x85
56+ assert got[1] == 0x69 // text(9)
57+ assert got[2..11] == 'Signature'.bytes()
58+ assert got[11] == 0x40 // empty body_protected
59+ assert got[12] == 0x43 // bstr of length 3
60+}
61+
62+// From HMac-enc-01.json (HS256 mac0):
63+// ToMac_hex = 84644D41433043A101054054...
64+fn test_mac_structure_mac0_matches_hmac_enc01_vector() {
65+ body_protected := hex.decode('A10105')! // {alg: 5} (HS256)
66+ want := hex.decode('84644D41433043A101054054546869732069732074686520636F6E74656E742E')!
67+ got := mac_structure_mac0(body_protected, []u8{}, sample_payload)
68+ assert got == want
69+}
70+
71+fn test_external_aad_is_signed_in_sig_structure() {
72+ body_protected := hex.decode('A10126')!
73+ external := 'application context'.bytes()
74+ with_ext := sig_structure_sign1(body_protected, external, sample_payload)
75+ without_ext := sig_structure_sign1(body_protected, []u8{}, sample_payload)
76+ assert with_ext != without_ext
77+}
@@ -0,0 +1,10 @@
1+// CBOR tag numbers reserved for COSE message types by RFC 9052 §2.
2+module cose
3+
4+// CBOR tag numbers identifying COSE message types. A COSE message MAY be
5+// emitted untagged (the recipient already knows the structure) or tagged
6+// with one of these values to make the message self-describing.
7+pub const tag_sign = u64(98) // COSE_Sign — RFC 9052 §4
8+pub const tag_sign1 = u64(18) // COSE_Sign1 — RFC 9052 §4.2
9+pub const tag_mac = u64(97) // COSE_Mac — RFC 9052 §6
10+pub const tag_mac0 = u64(17) // COSE_Mac0 — RFC 9052 §6.2
@@ -0,0 +1,38 @@
1+{
2+ "title":"HMAC-01: Direct key + HMAC-SHA256",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "mac":{
6+ "alg":"HS256",
7+ "protected":{
8+ "alg":"HS256"
9+ },
10+ "recipients":[
11+ {
12+ "unprotected":{
13+ "alg":"direct",
14+ "kid":"our-secret"
15+ },
16+ "key":{
17+ "kty":"oct",
18+ "kid":"our-secret",
19+ "use":"sig",
20+ "k":"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
21+ }
22+ }
23+ ]
24+ }
25+ },
26+ "intermediates":{
27+ "ToMac_hex":"84634D414343A101054054546869732069732074686520636F6E74656E742E",
28+ "CEK_hex":"849B57219DAE48DE646D07DBB533566E976686457C1491BE3A76DCEA6C427188",
29+ "recipients":[
30+ {
31+ }
32+ ]
33+ },
34+ "output":{
35+ "cbor_diag":"97([h'A10105', {}, h'546869732069732074686520636F6E74656E742E', h'2BDCC89F058216B8A208DDC6D8B54AA91F48BD63484986565105C9AD5A6682F6', [[h'', {1: -6, 4: h'6F75722D736563726574'}, h'']]])",
36+ "cbor":"D8618543A10105A054546869732069732074686520636F6E74656E742E58202BDCC89F058216B8A208DDC6D8B54AA91F48BD63484986565105C9AD5A6682F6818340A20125044A6F75722D73656372657440"
37+ }
38+}
@@ -0,0 +1,42 @@
1+{
2+ "title":"HMAC-04: Direct key + HMAC-SHA256 - Incorrect Tag ",
3+ "fail":true,
4+ "input":{
5+ "plaintext":"This is the content.",
6+ "mac":{
7+ "alg":"HS256",
8+ "protected":{
9+ "alg":"HS256"
10+ },
11+ "recipients":[
12+ {
13+ "unprotected":{
14+ "alg":"direct",
15+ "kid":"our-secret"
16+ },
17+ "key":{
18+ "kty":"oct",
19+ "kid":"our-secret",
20+ "use":"sig",
21+ "k":"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
22+ }
23+ }
24+ ]
25+ },
26+ "failures":{
27+ "ChangeTag":1
28+ }
29+ },
30+ "intermediates":{
31+ "ToMac_hex":"84634D414343A101054054546869732069732074686520636F6E74656E742E",
32+ "CEK_hex":"849B57219DAE48DE646D07DBB533566E976686457C1491BE3A76DCEA6C427188",
33+ "recipients":[
34+ {
35+ }
36+ ]
37+ },
38+ "output":{
39+ "cbor_diag":"97([h'A10105', {}, h'546869732069732074686520636F6E74656E742E', h'2BDCC89F058216B8A208DDC6D8B54AA91F48BD63484986565105C9AD5A6682F7', [[h'', {1: -6, 4: h'6F75722D736563726574'}, h'']]])",
40+ "cbor":"D8618543A10105A054546869732069732074686520636F6E74656E742E58202BDCC89F058216B8A208DDC6D8B54AA91F48BD63484986565105C9AD5A6682F7818340A20125044A6F75722D73656372657440"
41+ }
42+}
@@ -0,0 +1,38 @@
1+{
2+ "title":"HMAC-05: Direct key + HMAC-SHA256/64",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "mac":{
6+ "alg":"HS256/64",
7+ "protected":{
8+ "alg":"HS256/64"
9+ },
10+ "recipients":[
11+ {
12+ "unprotected":{
13+ "alg":"direct",
14+ "kid":"our-secret"
15+ },
16+ "key":{
17+ "kty":"oct",
18+ "kid":"our-secret",
19+ "use":"sig",
20+ "k":"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
21+ }
22+ }
23+ ]
24+ }
25+ },
26+ "intermediates":{
27+ "ToMac_hex":"84634D414343A101044054546869732069732074686520636F6E74656E742E",
28+ "CEK_hex":"849B57219DAE48DE646D07DBB533566E976686457C1491BE3A76DCEA6C427188",
29+ "recipients":[
30+ {
31+ }
32+ ]
33+ },
34+ "output":{
35+ "cbor_diag":"97([h'A10104', {}, h'546869732069732074686520636F6E74656E742E', h'6F35CAB779F77833', [[h'', {1: -6, 4: h'6F75722D736563726574'}, h'']]])",
36+ "cbor":"D8618543A10104A054546869732069732074686520636F6E74656E742E486F35CAB779F77833818340A20125044A6F75722D73656372657440"
37+ }
38+}
@@ -0,0 +1,38 @@
1+{
2+ "title":"HMAC-ENC-01: Direct key + HMAC-SHA256 - implicit",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "mac0":{
6+ "alg":"HS256",
7+ "protected":{
8+ "alg":"HS256"
9+ },
10+ "recipients":[
11+ {
12+ "unprotected":{
13+ "alg":"direct",
14+ "kid":"our-secret"
15+ },
16+ "key":{
17+ "kty":"oct",
18+ "kid":"our-secret",
19+ "use":"enc",
20+ "k":"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
21+ }
22+ }
23+ ]
24+ }
25+ },
26+ "intermediates":{
27+ "ToMac_hex":"84644D41433043A101054054546869732069732074686520636F6E74656E742E",
28+ "CEK_hex":"849B57219DAE48DE646D07DBB533566E976686457C1491BE3A76DCEA6C427188",
29+ "recipients":[
30+ {
31+ }
32+ ]
33+ },
34+ "output":{
35+ "cbor_diag":"17([h'A10105', {}, h'546869732069732074686520636F6E74656E742E', h'A1A848D3471F9D61EE49018D244C824772F223AD4F935293F1789FC3A08D8C58'])",
36+ "cbor":"D18443A10105A054546869732069732074686520636F6E74656E742E5820A1A848D3471F9D61EE49018D244C824772F223AD4F935293F1789FC3A08D8C58"
37+ }
38+}
@@ -0,0 +1,38 @@
1+{
2+ "title":"HMAC-ENC-02: Direct key + HMAC-SHA384 - implicit",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "mac0":{
6+ "alg":"HS384",
7+ "protected":{
8+ "alg":"HS384"
9+ },
10+ "recipients":[
11+ {
12+ "unprotected":{
13+ "alg":"direct",
14+ "kid":"sec-48"
15+ },
16+ "key":{
17+ "kty":"oct",
18+ "kid":"sec-48",
19+ "use":"enc",
20+ "k":"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYgAESIzd4iZqiEiIyQlJico"
21+ }
22+ }
23+ ]
24+ }
25+ },
26+ "intermediates":{
27+ "ToMac_hex":"84644D41433043A101064054546869732069732074686520636F6E74656E742E",
28+ "CEK_hex":"849B57219DAE48DE646D07DBB533566E976686457C1491BE3A76DCEA6C42718800112233778899AA2122232425262728",
29+ "recipients":[
30+ {
31+ }
32+ ]
33+ },
34+ "output":{
35+ "cbor_diag":"17([h'A10106', {}, h'546869732069732074686520636F6E74656E742E', h'998D26C6459AAEECF44ED20CE00C8CCEDF0A1F3D22A92FC05DB08C5AEB1CB594CAAF5A5C5E2E9D01CCE7E77A93AA8C62'])",
36+ "cbor":"D18443A10106A054546869732069732074686520636F6E74656E742E5830998D26C6459AAEECF44ED20CE00C8CCEDF0A1F3D22A92FC05DB08C5AEB1CB594CAAF5A5C5E2E9D01CCE7E77A93AA8C62"
37+ }
38+}
@@ -0,0 +1,38 @@
1+{
2+ "title":"HMAC-ENC-03: Direct key + HMAC-SHA512 - implicit",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "mac0":{
6+ "alg":"HS512",
7+ "protected":{
8+ "alg":"HS512"
9+ },
10+ "recipients":[
11+ {
12+ "unprotected":{
13+ "alg":"direct",
14+ "kid":"sec-64"
15+ },
16+ "key":{
17+ "kty":"oct",
18+ "kid":"sec-64",
19+ "use":"enc",
20+ "k":"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYgAESIzd4iZqiEiIyQlJicoqrvM3e7_paanqKmgsbKztA"
21+ }
22+ }
23+ ]
24+ }
25+ },
26+ "intermediates":{
27+ "ToMac_hex":"84644D41433043A101074054546869732069732074686520636F6E74656E742E",
28+ "CEK_hex":"849B57219DAE48DE646D07DBB533566E976686457C1491BE3A76DCEA6C42718800112233778899AA2122232425262728AABBCCDDEEFFA5A6A7A8A9A0B1B2B3B4",
29+ "recipients":[
30+ {
31+ }
32+ ]
33+ },
34+ "output":{
35+ "cbor_diag":"17([h'A10107', {}, h'546869732069732074686520636F6E74656E742E', h'4A555BF971F7C1891D9DDF304A1A132E2D6F817449474D813E6D04D65962BED8BBA70C17E1F5308FA39962959A4B9B8D7DA8E6D849B209DCD3E98CC0F11EDDF2'])",
36+ "cbor":"D18443A10107A054546869732069732074686520636F6E74656E742E58404A555BF971F7C1891D9DDF304A1A132E2D6F817449474D813E6D04D65962BED8BBA70C17E1F5308FA39962959A4B9B8D7DA8E6D849B209DCD3E98CC0F11EDDF2"
37+ }
38+}
@@ -0,0 +1,42 @@
1+{
2+ "title":"HMAC-ENC-04: Direct key + HMAC-SHA256 - Incorrect Tag - implicit",
3+ "fail":true,
4+ "input":{
5+ "plaintext":"This is the content.",
6+ "mac0":{
7+ "alg":"HS256",
8+ "protected":{
9+ "alg":"HS256"
10+ },
11+ "recipients":[
12+ {
13+ "unprotected":{
14+ "alg":"direct",
15+ "kid":"our-secret"
16+ },
17+ "key":{
18+ "kty":"oct",
19+ "kid":"our-secret",
20+ "use":"enc",
21+ "k":"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
22+ }
23+ }
24+ ]
25+ },
26+ "failures":{
27+ "ChangeTag":1
28+ }
29+ },
30+ "intermediates":{
31+ "ToMac_hex":"84644D41433043A101054054546869732069732074686520636F6E74656E742E",
32+ "CEK_hex":"849B57219DAE48DE646D07DBB533566E976686457C1491BE3A76DCEA6C427188",
33+ "recipients":[
34+ {
35+ }
36+ ]
37+ },
38+ "output":{
39+ "cbor_diag":"17([h'A10105', {}, h'546869732069732074686520636F6E74656E742E', h'A1A848D3471F9D61EE49018D244C824772F223AD4F935293F1789FC3A08D8C59'])",
40+ "cbor":"D18443A10105A054546869732069732074686520636F6E74656E742E5820A1A848D3471F9D61EE49018D244C824772F223AD4F935293F1789FC3A08D8C59"
41+ }
42+}
@@ -0,0 +1,38 @@
1+{
2+ "title":"HMAC-ENC-05: Direct key + HMAC-SHA256/64 - implicit",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "mac0":{
6+ "alg":"HS256/64",
7+ "protected":{
8+ "alg":"HS256/64"
9+ },
10+ "recipients":[
11+ {
12+ "unprotected":{
13+ "alg":"direct",
14+ "kid":"our-secret"
15+ },
16+ "key":{
17+ "kty":"oct",
18+ "kid":"our-secret",
19+ "use":"enc",
20+ "k":"hJtXIZ2uSN5kbQfbtTNWbpdmhkV8FJG-Onbc6mxCcYg"
21+ }
22+ }
23+ ]
24+ }
25+ },
26+ "intermediates":{
27+ "ToMac_hex":"84644D41433043A101044054546869732069732074686520636F6E74656E742E",
28+ "CEK_hex":"849B57219DAE48DE646D07DBB533566E976686457C1491BE3A76DCEA6C427188",
29+ "recipients":[
30+ {
31+ }
32+ ]
33+ },
34+ "output":{
35+ "cbor_diag":"17([h'A10104', {}, h'546869732069732074686520636F6E74656E742E', h'11F9E357975FB849'])",
36+ "cbor":"D18443A10104A054546869732069732074686520636F6E74656E742E4811F9E357975FB849"
37+ }
38+}
@@ -0,0 +1,32 @@
1+{
2+ "title":"ECDSA-01: ECDSA - P-256 - sign0",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "sign0":{
6+ "key":{
7+ "kty":"EC",
8+ "kid":"11",
9+ "crv":"P-256",
10+ "x":"usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
11+ "y":"IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
12+ "d":"V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
13+ },
14+ "unprotected":{
15+ "kid":"11"
16+ },
17+ "protected":{
18+ "alg":"ES256",
19+ "ctyp":0
20+ },
21+ "alg":"ES256"
22+ },
23+ "rng_description":"seed for signature"
24+ },
25+ "intermediates":{
26+ "ToBeSign_hex":"846A5369676E61747572653145A2012603004054546869732069732074686520636F6E74656E742E"
27+ },
28+ "output":{
29+ "cbor_diag":"18([h'A201260300', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'6520BBAF2081D7E0ED0F95F76EB0733D667005F7467CEC4B87B9381A6BA1EDE8E00DF29F32A37230F39A842A54821FDD223092819D7728EFB9D3A0080B75380B'])",
30+ "cbor":"D28445A201260300A10442313154546869732069732074686520636F6E74656E742E58406520BBAF2081D7E0ED0F95F76EB0733D667005F7467CEC4B87B9381A6BA1EDE8E00DF29F32A37230F39A842A54821FDD223092819D7728EFB9D3A0080B75380B"
31+ }
32+}
@@ -0,0 +1,32 @@
1+{
2+ "title":"ECDSA-03: ECDSA - P-512 - sign0",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "sign0":{
6+ "alg":"ES512",
7+ "key":{
8+ "kty":"EC",
9+ "kid":"[email protected]",
10+ "use":"sig",
11+ "crv":"P-521",
12+ "x":"AHKZLLOsCOzz5cY97ewNUajB957y-C-U88c3v13nmGZx6sYl_oJXu9A5RkTKqjqvjyekWF-7ytDyRXYgCF5cj0Kt",
13+ "y":"AdymlHvOiLxXkEhayXQnNCvDX4h9htZaCJN34kfmC6pV5OhQHiraVySsUdaQkAgDPrwQrJmbnX9cwlGfP-HqHZR1",
14+ "d":"AAhRON2r9cqXX1hg-RoI6R1tX5p2rUAYdmpHZoC1XNM56KtscrX6zbKipQrCW9CGZH3T4ubpnoTKLDYJ_fF3_rJt"
15+ },
16+ "protected":{
17+ "alg":"ES512"
18+ },
19+ "unprotected":{
20+ "kid":"[email protected]"
21+ }
22+ },
23+ "rng_description":"seed for signature"
24+ },
25+ "intermediates":{
26+ "ToBeSign_hex":"846A5369676E61747572653144A10138234054546869732069732074686520636F6E74656E742E"
27+ },
28+ "output":{
29+ "cbor_diag":"18([h'A1013823', {4: h'62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C65'}, h'546869732069732074686520636F6E74656E742E', h'01664DD6962091B5100D6E1833D503539330EC2BC8FD3E8996950CE9F70259D9A30F73794F603B0D3E7C5E9C4C2A57E10211F76E79DF8FFD1B79D7EF5B9FA7DA109001965FA2D37E093BB13C040399C467B3B9908C09DB2B0F1F4996FE07BB02AAA121A8E1C671F3F997ADE7D651081017057BD3A8A5FBF394972EA71CFDC15E6F8FE2E1'])",
30+ "cbor":"D28444A1013823A104581E62696C626F2E62616767696E7340686F626269746F6E2E6578616D706C6554546869732069732074686520636F6E74656E742E588401664DD6962091B5100D6E1833D503539330EC2BC8FD3E8996950CE9F70259D9A30F73794F603B0D3E7C5E9C4C2A57E10211F76E79DF8FFD1B79D7EF5B9FA7DA109001965FA2D37E093BB13C040399C467B3B9908C09DB2B0F1F4996FE07BB02AAA121A8E1C671F3F997ADE7D651081017057BD3A8A5FBF394972EA71CFDC15E6F8FE2E1"
31+ }
32+}
@@ -0,0 +1,31 @@
1+{
2+ "title":"ECDSA-sig-01: ECDSA - P-256 w/ SHA-512 - implicit",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "sign0":{
6+ "key":{
7+ "kty":"EC",
8+ "kid":"11",
9+ "crv":"P-256",
10+ "x":"usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
11+ "y":"IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
12+ "d":"V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
13+ },
14+ "unprotected":{
15+ "kid":"11"
16+ },
17+ "protected":{
18+ "alg":"ES512"
19+ },
20+ "alg":"ES512"
21+ },
22+ "rng_description":"seed for signature"
23+ },
24+ "intermediates":{
25+ "ToBeSign_hex":"846A5369676E61747572653144A10138234054546869732069732074686520636F6E74656E742E"
26+ },
27+ "output":{
28+ "cbor_diag":"18([h'A1013823', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'EB18B84ED674284E5ED861C3943E101BED5DB9F560C0F0292B34362990D1C59B10DF7946CBC6CA3DCBD6C17A6DD1D711F50337BAA6B4FCFAE0EFC70E52C1DE0F'])",
29+ "cbor":"D28444A1013823A10442313154546869732069732074686520636F6E74656E742E5840EB18B84ED674284E5ED861C3943E101BED5DB9F560C0F0292B34362990D1C59B10DF7946CBC6CA3DCBD6C17A6DD1D711F50337BAA6B4FCFAE0EFC70E52C1DE0F"
30+ }
31+}
@@ -0,0 +1,40 @@
1+{
2+ "title":"EdDSA-01: EdDSA - 25519",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "sign":{
6+ "protected":{
7+ "ctyp":0
8+ },
9+ "signers":[
10+ {
11+ "key":{
12+ "kty":"OKP",
13+ "kid":"11",
14+ "crv":"Ed25519",
15+ "x_hex":"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
16+ "d_hex":"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
17+ },
18+ "unprotected":{
19+ "kid":"11"
20+ },
21+ "protected":{
22+ "alg":"EdDSA"
23+ }
24+ }
25+ ]
26+ },
27+ "rng_description":"seed for signature"
28+ },
29+ "intermediates":{
30+ "signers":[
31+ {
32+ "ToBeSign_hex":"85695369676E617475726543A1030043A101274054546869732069732074686520636F6E74656E742E"
33+ }
34+ ]
35+ },
36+ "output":{
37+ "cbor_diag":"98([h'A10300', {}, h'546869732069732074686520636F6E74656E742E', [[h'A10127', {4: h'3131'}, h'77F3EACD11852C4BF9CB1D72FABE6B26FBA1D76092B2B5B7EC83B83557652264E69690DBC1172DDC0BF88411C0D25A507FDB247A20C40D5E245FABD3FC9EC106']]])",
38+ "cbor":"D8628443A10300A054546869732069732074686520636F6E74656E742E818343A10127A104423131584077F3EACD11852C4BF9CB1D72FABE6B26FBA1D76092B2B5B7EC83B83557652264E69690DBC1172DDC0BF88411C0D25A507FDB247A20C40D5E245FABD3FC9EC106"
39+ }
40+}
@@ -0,0 +1,37 @@
1+{
2+ "title":"EdDSA-02: EdDSA - 448",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "sign":{
6+ "signers":[
7+ {
8+ "key":{
9+ "kty":"OKP",
10+ "kid":"ed448",
11+ "crv":"Ed448",
12+ "x_hex":"5fd7449b59b461fd2ce787ec616ad46a1da1342485a70e1f8a0ea75d80e96778edf124769b46c7061bd6783df1e50f6cd1fa1abeafe8256180",
13+ "d_hex":"6c82a562cb808d10d632be89c8513ebf6c929f34ddfa8c9f63c9960ef6e348a3528c8a3fcc2f044e39a3fc5b94492f8f032e7549a20098f95b"
14+ },
15+ "unprotected":{
16+ "kid":"ed448"
17+ },
18+ "protected":{
19+ "alg":"EdDSA"
20+ }
21+ }
22+ ]
23+ },
24+ "rng_description":"seed for signature"
25+ },
26+ "intermediates":{
27+ "signers":[
28+ {
29+ "ToBeSign_hex":"85695369676E61747572654043A101274054546869732069732074686520636F6E74656E742E"
30+ }
31+ ]
32+ },
33+ "output":{
34+ "cbor_diag":"98([h'', {}, h'546869732069732074686520636F6E74656E742E', [[h'A10127', {4: h'6564343438'}, h'ABF04F4BC7DFACF70C20C34A3CFBD27719911DC8518B2D67BF6AF62895D0FA1E6A1CB8B47AD1297C0E9C34BEB34E50DFFEF14350EBD57842807D54914111150F698543B0A5E1DA1DB79632C6415CE18EF74EDAEA680B0C8881439D869171481D78E2F7D26340C293C2ECDED8DE1425851900']]])",
35+ "cbor":"D8628440A054546869732069732074686520636F6E74656E742E818343A10127A1044565643434385872ABF04F4BC7DFACF70C20C34A3CFBD27719911DC8518B2D67BF6AF62895D0FA1E6A1CB8B47AD1297C0E9C34BEB34E50DFFEF14350EBD57842807D54914111150F698543B0A5E1DA1DB79632C6415CE18EF74EDAEA680B0C8881439D869171481D78E2F7D26340C293C2ECDED8DE1425851900"
36+ }
37+}
@@ -0,0 +1,31 @@
1+{
2+ "title":"EdDSA-01: EdDSA - 25519 - sign0",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "sign0":{
6+ "key":{
7+ "kty":"OKP",
8+ "kid":"11",
9+ "crv":"Ed25519",
10+ "x_hex":"d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a",
11+ "d_hex":"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
12+ },
13+ "unprotected":{
14+ "kid":"11"
15+ },
16+ "protected":{
17+ "alg":"EdDSA",
18+ "ctyp":0
19+ },
20+ "alg":"EdDSA"
21+ },
22+ "rng_description":"seed for signature"
23+ },
24+ "intermediates":{
25+ "ToBeSign_hex":"846A5369676E61747572653145A2012703004054546869732069732074686520636F6E74656E742E"
26+ },
27+ "output":{
28+ "cbor_diag":"18([h'A201270300', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'7142FD2FF96D56DB85BEE905A76BA1D0B7321A95C8C4D3607C5781932B7AFB8711497DFA751BF40B58B3BCC32300B1487F3DB34085EEF013BF08F4A44D6FEF0D'])",
29+ "cbor":"D28445A201270300A10442313154546869732069732074686520636F6E74656E742E58407142FD2FF96D56DB85BEE905A76BA1D0B7321A95C8C4D3607C5781932B7AFB8711497DFA751BF40B58B3BCC32300B1487F3DB34085EEF013BF08F4A44D6FEF0D"
30+ }
31+}
@@ -0,0 +1,30 @@
1+{
2+ "title":"EdDSA-sig-02: EdDSA - 448 - sign1",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "sign0":{
6+ "key":{
7+ "kty":"OKP",
8+ "kid":"ed448",
9+ "crv":"Ed448",
10+ "x_hex":"5fd7449b59b461fd2ce787ec616ad46a1da1342485a70e1f8a0ea75d80e96778edf124769b46c7061bd6783df1e50f6cd1fa1abeafe8256180",
11+ "d_hex":"6c82a562cb808d10d632be89c8513ebf6c929f34ddfa8c9f63c9960ef6e348a3528c8a3fcc2f044e39a3fc5b94492f8f032e7549a20098f95b"
12+ },
13+ "unprotected":{
14+ "kid":"ed448"
15+ },
16+ "protected":{
17+ "alg":"EdDSA"
18+ },
19+ "alg":"EdDSA"
20+ },
21+ "rng_description":"seed for signature"
22+ },
23+ "intermediates":{
24+ "ToBeSign_hex":"846A5369676E61747572653143A101274054546869732069732074686520636F6E74656E742E"
25+ },
26+ "output":{
27+ "cbor_diag":"18([h'A10127', {4: h'6564343438'}, h'546869732069732074686520636F6E74656E742E', h'988240A3A2F189BD486DE14AA77F54686C576A09F2E7ED9BAE910DF9139C2AC3BE7C27B7E10A20FA17C9D57D3510A2CF1F634BC0345AB9BE00849842171D1E9E98B2674C0E38BFCF6C557A1692B01B71015A47AC9F7748840CAD1DA80CBB5B349309FEBB912672B377C8B2072AF1598B3700'])",
28+ "cbor":"D28443A10127A10445656434343854546869732069732074686520636F6E74656E742E5872988240A3A2F189BD486DE14AA77F54686C576A09F2E7ED9BAE910DF9139C2AC3BE7C27B7E10A20FA17C9D57D3510A2CF1F634BC0345AB9BE00849842171D1E9E98B2674C0E38BFCF6C557A1692B01B71015A47AC9F7748840CAD1DA80CBB5B349309FEBB912672B377C8B2072AF1598B3700"
29+ }
30+}
@@ -0,0 +1,35 @@
1+{
2+ "title":"sign-fail-01: Wrong CBOR Tag",
3+ "fail":true,
4+ "input":{
5+ "plaintext":"This is the content.",
6+ "sign0":{
7+ "key":{
8+ "kty":"EC",
9+ "kid":"11",
10+ "crv":"P-256",
11+ "x":"usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
12+ "y":"IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
13+ "d":"V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
14+ },
15+ "unprotected":{
16+ "kid":"11"
17+ },
18+ "protected":{
19+ "alg":"ES256"
20+ },
21+ "alg":"ES256"
22+ },
23+ "failures":{
24+ "ChangeCBORTag":998
25+ },
26+ "rng_description":"seed for signature"
27+ },
28+ "intermediates":{
29+ "ToBeSign_hex":"846A5369676E61747572653143A101264054546869732069732074686520636F6E74656E742E"
30+ },
31+ "output":{
32+ "cbor_diag":"998([h'A10126', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'8EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36'])",
33+ "cbor":"D903E68443A10126A10442313154546869732069732074686520636F6E74656E742E58408EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36"
34+ }
35+}
@@ -0,0 +1,35 @@
1+{
2+ "title":"sign-fail-02: Change signature",
3+ "fail":true,
4+ "input":{
5+ "plaintext":"This is the content.",
6+ "sign0":{
7+ "key":{
8+ "kty":"EC",
9+ "kid":"11",
10+ "crv":"P-256",
11+ "x":"usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
12+ "y":"IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
13+ "d":"V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
14+ },
15+ "unprotected":{
16+ "kid":"11"
17+ },
18+ "protected":{
19+ "alg":"ES256"
20+ },
21+ "alg":"ES256"
22+ },
23+ "failures":{
24+ "ChangeTag":1
25+ },
26+ "rng_description":"seed for signature"
27+ },
28+ "intermediates":{
29+ "ToBeSign_hex":"846A5369676E61747572653143A101264054546869732069732074686520636F6E74656E742E"
30+ },
31+ "output":{
32+ "cbor_diag":"18([h'A10126', {4: h'3131'}, h'546869732069732074686520636F6E74656E742F', h'8EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36'])",
33+ "cbor":"D28443A10126A10442313154546869732069732074686520636F6E74656E742F58408EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36"
34+ }
35+}
@@ -0,0 +1,37 @@
1+{
2+ "title":"sign-fail-03: Change Sign Algorithm",
3+ "fail":true,
4+ "input":{
5+ "plaintext":"This is the content.",
6+ "sign0":{
7+ "key":{
8+ "kty":"EC",
9+ "kid":"11",
10+ "crv":"P-256",
11+ "x":"usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
12+ "y":"IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
13+ "d":"V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
14+ },
15+ "unprotected":{
16+ "kid":"11"
17+ },
18+ "protected":{
19+ "alg":"ES256"
20+ },
21+ "alg":"ES256"
22+ },
23+ "failures":{
24+ "ChangeAttr":{
25+ "alg":-999
26+ }
27+ },
28+ "rng_description":"seed for signature"
29+ },
30+ "intermediates":{
31+ "ToBeSign_hex":"846A5369676E61747572653143A101264054546869732069732074686520636F6E74656E742E"
32+ },
33+ "output":{
34+ "cbor_diag":"18([h'A1013903E6', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'8EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36'])",
35+ "cbor":"D28445A1013903E6A10442313154546869732069732074686520636F6E74656E742E58408EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36"
36+ }
37+}
@@ -0,0 +1,37 @@
1+{
2+ "title":"sign-fail-04: Change Sign Algorithm",
3+ "fail":true,
4+ "input":{
5+ "plaintext":"This is the content.",
6+ "sign0":{
7+ "key":{
8+ "kty":"EC",
9+ "kid":"11",
10+ "crv":"P-256",
11+ "x":"usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
12+ "y":"IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
13+ "d":"V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
14+ },
15+ "unprotected":{
16+ "kid":"11"
17+ },
18+ "protected":{
19+ "alg":"ES256"
20+ },
21+ "alg":"ES256"
22+ },
23+ "failures":{
24+ "ChangeAttr":{
25+ "alg":"unknown"
26+ }
27+ },
28+ "rng_description":"seed for signature"
29+ },
30+ "intermediates":{
31+ "ToBeSign_hex":"846A5369676E61747572653143A101264054546869732069732074686520636F6E74656E742E"
32+ },
33+ "output":{
34+ "cbor_diag":"18([h'A10167756E6B6E6F776E', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'8EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36'])",
35+ "cbor":"D2844AA10167756E6B6E6F776EA10442313154546869732069732074686520636F6E74656E742E58408EB33E4CA31D1C465AB05AAC34CC6B23D58FEF5C083106C4D25A91AEF0B0117E2AF9A291AA32E14AB834DC56ED2A223444547E01F11D3B0916E5A4C345CACB36"
36+ }
37+}
@@ -0,0 +1,32 @@
1+{
2+ "title":"sign-pass-01: Redo protected",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "sign0":{
6+ "key":{
7+ "kty":"EC",
8+ "kid":"11",
9+ "crv":"P-256",
10+ "x":"usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
11+ "y":"IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
12+ "d":"V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
13+ },
14+ "unprotected":{
15+ "kid":"11",
16+ "alg":"ES256"
17+ },
18+ "alg":"ES256"
19+ },
20+ "failures":{
21+ "ChangeProtected":"a0"
22+ },
23+ "rng_description":"seed for signature"
24+ },
25+ "intermediates":{
26+ "ToBeSign_hex":"846A5369676E617475726531404054546869732069732074686520636F6E74656E742E"
27+ },
28+ "output":{
29+ "cbor_diag":"18([h'A0', {1: -7, 4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'87DB0D2E5571843B78AC33ECB2830DF7B6E0A4D5B7376DE336B23C591C90C425317E56127FBE04370097CE347087B233BF722B64072BEB4486BDA4031D27244F'])",
30+ "cbor":"D28441A0A201260442313154546869732069732074686520636F6E74656E742E584087DB0D2E5571843B78AC33ECB2830DF7B6E0A4D5B7376DE336B23C591C90C425317E56127FBE04370097CE347087B233BF722B64072BEB4486BDA4031D27244F"
31+ }
32+}
@@ -0,0 +1,32 @@
1+{
2+ "title":"sign-pass-02: External",
3+ "input":{
4+ "plaintext":"This is the content.",
5+ "sign0":{
6+ "key":{
7+ "kty":"EC",
8+ "kid":"11",
9+ "crv":"P-256",
10+ "x":"usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
11+ "y":"IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
12+ "d":"V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM"
13+ },
14+ "unprotected":{
15+ "kid":"11"
16+ },
17+ "protected":{
18+ "alg":"ES256"
19+ },
20+ "alg":"ES256",
21+ "external":"11aa22bb33cc44dd55006699"
22+ },
23+ "rng_description":"seed for signature"
24+ },
25+ "intermediates":{
26+ "ToBeSign_hex":"846A5369676E61747572653143A101264C11AA22BB33CC44DD5500669954546869732069732074686520636F6E74656E742E"
27+ },
28+ "output":{
29+ "cbor_diag":"18([h'A10126', {4: h'3131'}, h'546869732069732074686520636F6E74656E742E', h'10729CD711CB3813D8D8E944A8DA7111E7B258C9BDCA6135F7AE1ADBEE9509891267837E1E33BD36C150326AE62755C6BD8E540C3E8F92D7D225E8DB72B8820B'])",
30+ "cbor":"D28443A10126A10442313154546869732069732074686520636F6E74656E742E584010729CD711CB3813D8D8E944A8DA7111E7B258C9BDCA6135F7AE1ADBEE9509891267837E1E33BD36C150326AE62755C6BD8E540C3E8F92D7D225E8DB72B8820B"
31+ }
32+}
@@ -0,0 +1,119 @@
1+# `encoding.cwt`
2+
3+CBOR Web Tokens — pure-V implementation of [RFC 8392][rfc8392], the
4+CBOR analogue of JSON Web Tokens. A CWT is a Claims Set encoded as
5+CBOR and wrapped in a COSE message (COSE_Sign1 or COSE_Mac0 in the
6+current set of supported wrappers).
7+
8+[rfc8392]: https://www.rfc-editor.org/rfc/rfc8392
9+
10+This module is a thin layer over [`encoding.cose`](../cose/README.md)
11+— all the cryptography is handled there. CWT-specific concerns are:
12+
13+- Modelling the standard claims (`iss`, `sub`, `aud`, `exp`, `nbf`,
14+ `iat`, `cti`) as typed fields on `ClaimsSet`.
15+- Serialising/parsing the CBOR claims map.
16+- Adding/removing the optional outer CBOR tag 61 (`tag_cwt`).
17+
18+## Quick examples
19+
20+### Sign a CWT (ES256)
21+
22+```v
23+import encoding.cwt
24+import encoding.cose
25+import encoding.hex
26+
27+fn main() {
28+ x := hex.decode('143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f')!
29+ y := hex.decode('60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9')!
30+ d := hex.decode('6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19')!
31+ priv := cose.Key.ec2_private(.p_256, x, y, d)
32+ pub_key := cose.Key.ec2_public(.p_256, x, y)
33+
34+ claims := cwt.ClaimsSet{
35+ iss: 'coap://as.example.com'
36+ sub: 'erikw'
37+ aud: ['coap://light.example.com']
38+ exp: 1444064944
39+ iat: 1443944944
40+ }
41+ token := cwt.sign(claims, priv,
42+ protected: cose.Headers{
43+ algorithm: .es256
44+ }
45+ )!
46+
47+ got := cwt.verify(token, pub_key)!
48+ assert (got.iss or { '' }) == 'coap://as.example.com'
49+}
50+```
51+
52+### Verify a CWT and check expiry
53+
54+`cwt.verify` only authenticates the token; the `exp` / `nbf` window
55+must be enforced by the application. Use `validate_time` right after
56+verification:
57+
58+```v ignore
59+import time
60+
61+claims := cwt.verify(token, pub_key)!
62+claims.validate_time(time.now().unix) or {
63+ if err is cwt.ClaimExpired { return error('token expired') }
64+ if err is cwt.ClaimNotYetValid { return error('token not yet valid') }
65+ return err
66+}
67+```
68+
69+### MAC a CWT (HMAC 256/64)
70+
71+```v ignore
72+key := cose.Key.symmetric(secret_bytes)
73+token := cwt.mac(claims, key, protected: cose.Headers{ algorithm: .hmac_256_64 })!
74+got := cwt.verify_mac(token, key)!
75+```
76+
77+## Outer tag 61
78+
79+The default for `sign` and `mac` is to wrap the COSE message in CBOR
80+tag 61 (the CWT marker, RFC 8392 §6). Set `tagged_cwt: false` in
81+options to emit just the inner COSE message — handy for contexts
82+where the surrounding protocol already disambiguates the payload
83+type. Verification accepts both forms.
84+
85+## Standard claims modelled
86+
87+| Claim | Label | V field | Type |
88+| ----- | ----- | --------- | ------ |
89+| iss | 1 | `iss` | `?string` |
90+| sub | 2 | `sub` | `?string` |
91+| aud | 3 | `aud` | `[]string` (single = string form on the wire) |
92+| exp | 4 | `exp` | `?i64` Unix seconds |
93+| nbf | 5 | `nbf` | `?i64` |
94+| iat | 6 | `iat` | `?i64` |
95+| cti | 7 | `cti` | `?[]u8` |
96+
97+Application-specific claims go into `extra_int_claims` /
98+`extra_text_claims`, with raw `cbor.Value` payloads.
99+
100+## Time handling
101+
102+`exp`, `nbf` and `iat` are decoded into `i64` Unix seconds. Encoded
103+output uses the integer form of NumericDate (RFC 8392 §3); the
104+fractional form (CBOR float) is accepted on decode for interop.
105+
106+`validate_time(now_unix i64)` does the standard `nbf` ≤ `now` < `exp`
107+check and returns a `ClaimExpired` or `ClaimNotYetValid` typed error.
108+The check is *not* run automatically by `verify` / `verify_mac` so
109+that callers stay in control of the clock source (real time, fixed
110+test time, replay-safe context, etc.).
111+
112+For one-off boolean checks: `claims.expired(now_unix)` and
113+`claims.not_yet_valid(now_unix)`.
114+
115+## See also
116+
117+- [`encoding.cose`](../cose/README.md) — the underlying signature/MAC
118+ primitives.
119+- [`encoding.cbor`](../cbor/README.md) — the CBOR codec.
@@ -0,0 +1,273 @@
1+// CWT Claims Set as defined by RFC 8392 §3 and §4. The well-known
2+// claims are exposed as typed fields; application-specific claims can
3+// be carried via `extra_int_claims` / `extra_text_claims`.
4+module cwt
5+
6+import encoding.cbor
7+
8+// Standard claim labels from RFC 8392 §3, table 1, and the IANA "CBOR
9+// Web Token (CWT) Claims" registry.
10+const claim_iss = i64(1)
11+const claim_sub = i64(2)
12+const claim_aud = i64(3)
13+const claim_exp = i64(4)
14+const claim_nbf = i64(5)
15+const claim_iat = i64(6)
16+const claim_cti = i64(7)
17+
18+// CBOR tag 61 (RFC 8392 §6) marks a CWT, distinguishing it from a
19+// generic COSE message.
20+pub const tag_cwt = u64(61)
21+
22+// ClaimsSet is the V representation of a CWT Claims Set (RFC 8392 §3).
23+// Time-valued claims (`exp`, `nbf`, `iat`) are stored as Unix seconds
24+// (i64). RFC 8392 also allows fractional NumericDate via CBOR floats;
25+// the encoder always emits the integer form (what every real-world
26+// deployment uses) but the decoder accepts both.
27+pub struct ClaimsSet {
28+pub mut:
29+ iss ?string
30+ sub ?string
31+ // aud — RFC 7519 / 8392 allow either a single string or an array.
32+ // We model both as a slice for uniformity: empty = no audience,
33+ // single-element = string form on the wire, multi-element = array.
34+ aud []string
35+ exp ?i64
36+ nbf ?i64
37+ iat ?i64
38+ cti ?[]u8
39+ // extra_int_claims carries unmodelled integer-labelled claims.
40+ extra_int_claims []ClaimEntry
41+ // extra_text_claims carries unmodelled text-labelled claims.
42+ extra_text_claims []TextClaimEntry
43+}
44+
45+// ClaimEntry is one (int label, value) pair.
46+pub struct ClaimEntry {
47+pub:
48+ label i64
49+ value cbor.Value
50+}
51+
52+// TextClaimEntry is one (text label, value) pair.
53+pub struct TextClaimEntry {
54+pub:
55+ label string
56+ value cbor.Value
57+}
58+
59+// encode returns the canonical CBOR encoding of the claims set as a
60+// CBOR map. The output is the bytes that go into the
61+// `payload` slot of the surrounding COSE message.
62+pub fn (c ClaimsSet) encode() ![]u8 {
63+ mut pairs := []cbor.MapPair{cap: 8 + c.extra_int_claims.len + c.extra_text_claims.len}
64+ if iss := c.iss {
65+ pairs << cbor.MapPair{
66+ key: cbor.new_int(claim_iss)
67+ value: cbor.new_text(iss)
68+ }
69+ }
70+ if sub := c.sub {
71+ pairs << cbor.MapPair{
72+ key: cbor.new_int(claim_sub)
73+ value: cbor.new_text(sub)
74+ }
75+ }
76+ if c.aud.len == 1 {
77+ pairs << cbor.MapPair{
78+ key: cbor.new_int(claim_aud)
79+ value: cbor.new_text(c.aud[0])
80+ }
81+ } else if c.aud.len > 1 {
82+ mut arr := cbor.Array{}
83+ for a in c.aud {
84+ arr.elements << cbor.new_text(a)
85+ }
86+ pairs << cbor.MapPair{
87+ key: cbor.new_int(claim_aud)
88+ value: arr
89+ }
90+ }
91+ if exp := c.exp {
92+ pairs << cbor.MapPair{
93+ key: cbor.new_int(claim_exp)
94+ value: cbor.new_int(exp)
95+ }
96+ }
97+ if nbf := c.nbf {
98+ pairs << cbor.MapPair{
99+ key: cbor.new_int(claim_nbf)
100+ value: cbor.new_int(nbf)
101+ }
102+ }
103+ if iat := c.iat {
104+ pairs << cbor.MapPair{
105+ key: cbor.new_int(claim_iat)
106+ value: cbor.new_int(iat)
107+ }
108+ }
109+ if cti := c.cti {
110+ pairs << cbor.MapPair{
111+ key: cbor.new_int(claim_cti)
112+ value: cbor.new_bytes(cti)
113+ }
114+ }
115+ for e in c.extra_int_claims {
116+ pairs << cbor.MapPair{
117+ key: cbor.new_int(e.label)
118+ value: e.value
119+ }
120+ }
121+ for e in c.extra_text_claims {
122+ pairs << cbor.MapPair{
123+ key: cbor.new_text(e.label)
124+ value: e.value
125+ }
126+ }
127+ return cbor.encode(cbor.Value(cbor.Map{ pairs: pairs }), cbor.EncodeOpts{
128+ canonical: true
129+ })!
130+}
131+
132+// ClaimsSet.decode parses a CBOR-encoded Claims Set. Unknown claims
133+// are kept in `extra_int_claims` / `extra_text_claims`.
134+pub fn ClaimsSet.decode(data []u8) !ClaimsSet {
135+ v := cbor.decode[cbor.Value](data, cbor.DecodeOpts{})!
136+ m := if v is cbor.Map {
137+ v
138+ } else {
139+ return error('cwt: claims set is not a CBOR map')
140+ }
141+ mut c := ClaimsSet{}
142+ for pair in m.pairs {
143+ if int_key := pair.key.as_int() {
144+ match int_key {
145+ claim_iss {
146+ c.iss = pair.value.as_string() or { return error('cwt: iss is not text') }
147+ }
148+ claim_sub {
149+ c.sub = pair.value.as_string() or { return error('cwt: sub is not text') }
150+ }
151+ claim_aud {
152+ if s := pair.value.as_string() {
153+ c.aud = [s]
154+ } else if items := pair.value.as_array() {
155+ mut auds := []string{cap: items.len}
156+ for it in items {
157+ s := it.as_string() or {
158+ return error('cwt: aud array contains non-text')
159+ }
160+ auds << s
161+ }
162+ c.aud = auds
163+ } else {
164+ return error('cwt: aud is neither text nor array of text')
165+ }
166+ }
167+ claim_exp {
168+ c.exp = decode_numeric_date(pair.value)!
169+ }
170+ claim_nbf {
171+ c.nbf = decode_numeric_date(pair.value)!
172+ }
173+ claim_iat {
174+ c.iat = decode_numeric_date(pair.value)!
175+ }
176+ claim_cti {
177+ c.cti = pair.value.as_bytes() or { return error('cwt: cti is not bstr') }
178+ }
179+ else {
180+ c.extra_int_claims << ClaimEntry{
181+ label: int_key
182+ value: pair.value
183+ }
184+ }
185+ }
186+ } else if str_key := pair.key.as_string() {
187+ c.extra_text_claims << TextClaimEntry{
188+ label: str_key
189+ value: pair.value
190+ }
191+ } else {
192+ return error('cwt: claim label is neither int nor text')
193+ }
194+ }
195+ return c
196+}
197+
198+// decode_numeric_date accepts the integer or float forms of NumericDate
199+// (RFC 8392 §3) and returns Unix seconds as i64.
200+fn decode_numeric_date(v cbor.Value) !i64 {
201+ if i := v.as_int() {
202+ return i
203+ }
204+ if f := v.as_float() {
205+ return i64(f)
206+ }
207+ return error('cwt: NumericDate is neither integer nor float')
208+}
209+
210+// expired reports whether the claims set's `exp` (expiration time)
211+// is at or before `now_unix`. Returns `false` when `exp` is absent —
212+// per RFC 8392 §3.1.4 a CWT without `exp` does not expire on its own.
213+//
214+// Pass `time.now().unix` for the wall-clock check; pass a fixed
215+// timestamp for unit tests or replay-safe contexts.
216+pub fn (c ClaimsSet) expired(now_unix i64) bool {
217+ exp := c.exp or { return false }
218+ return now_unix >= exp
219+}
220+
221+// not_yet_valid reports whether the claims set's `nbf` (not-before)
222+// is in the future relative to `now_unix`. Returns `false` when
223+// `nbf` is absent.
224+pub fn (c ClaimsSet) not_yet_valid(now_unix i64) bool {
225+ nbf := c.nbf or { return false }
226+ return now_unix < nbf
227+}
228+
229+// validate_time runs the `nbf`/`exp` checks against `now_unix` and
230+// returns a typed error when the token is outside its validity window,
231+// or `none` when both checks pass (or the relevant claims are absent).
232+// This is the convenience helper most application code wants right
233+// after `cwt.verify(...)`.
234+pub fn (c ClaimsSet) validate_time(now_unix i64) ! {
235+ if c.not_yet_valid(now_unix) {
236+ return ClaimNotYetValid{
237+ nbf: c.nbf or { 0 }
238+ now: now_unix
239+ }
240+ }
241+ if c.expired(now_unix) {
242+ return ClaimExpired{
243+ exp: c.exp or { 0 }
244+ now: now_unix
245+ }
246+ }
247+}
248+
249+// ClaimExpired is returned by `validate_time` when `now >= exp`.
250+pub struct ClaimExpired {
251+ Error
252+pub:
253+ exp i64
254+ now i64
255+}
256+
257+// msg formats a ClaimExpired for `IError.msg()`.
258+pub fn (e &ClaimExpired) msg() string {
259+ return 'cwt: token expired (exp=${e.exp}, now=${e.now})'
260+}
261+
262+// ClaimNotYetValid is returned by `validate_time` when `now < nbf`.
263+pub struct ClaimNotYetValid {
264+ Error
265+pub:
266+ nbf i64
267+ now i64
268+}
269+
270+// msg formats a ClaimNotYetValid for `IError.msg()`.
271+pub fn (e &ClaimNotYetValid) msg() string {
272+ return 'cwt: token not yet valid (nbf=${e.nbf}, now=${e.now})'
273+}
@@ -0,0 +1,128 @@
1+// CBOR Web Token (CWT) — RFC 8392 — module entry point.
2+//
3+// A CWT is a Claims Set serialised as CBOR and wrapped in a COSE
4+// message (typically COSE_Sign1 or COSE_Mac0). The optional CBOR tag
5+// 61 is used to distinguish a CWT from an arbitrary signed/MACed CBOR
6+// blob.
7+//
8+// This module is a thin layer on top of `encoding.cose`: the heavy
9+// lifting (signing, MACing, header handling) lives there. The two
10+// CWT-specific concerns are: (1) converting a `ClaimsSet` to/from the
11+// CBOR payload, and (2) handling the optional outer tag 61.
12+module cwt
13+
14+import encoding.cbor
15+import encoding.cose
16+
17+// SignOptions controls the signing of a CWT. The same fields as
18+// `cose.Sign1Options` plus a CWT-specific `tagged_cwt` flag controlling
19+// the outer tag 61 wrapper.
20+@[params]
21+pub struct SignOptions {
22+pub:
23+ protected cose.Headers
24+ unprotected cose.Headers
25+ external_aad []u8
26+ // untagged_cose disables the inner COSE tag 18 wrapper. The default
27+ // (tagged) is what every interop test in RFC 8392 uses.
28+ untagged_cose bool
29+ // tagged_cwt wraps the COSE message in CBOR tag 61. The default is
30+ // `true`. RFC 8392 §6 RECOMMENDS the tag for self-describing
31+ // payloads but allows omitting it when the context already
32+ // disambiguates.
33+ tagged_cwt bool = true
34+}
35+
36+// VerifyOptions mirrors SignOptions for the verification side.
37+@[params]
38+pub struct VerifyOptions {
39+pub:
40+ external_aad []u8
41+}
42+
43+// MacOptions controls the MACing of a CWT (Mac0 mode).
44+@[params]
45+pub struct MacOptions {
46+pub:
47+ protected cose.Headers
48+ unprotected cose.Headers
49+ external_aad []u8
50+ untagged_cose bool
51+ tagged_cwt bool = true
52+}
53+
54+// VerifyMacOptions mirrors MacOptions for the verification side.
55+@[params]
56+pub struct VerifyMacOptions {
57+pub:
58+ external_aad []u8
59+}
60+
61+// sign produces a signed CWT from `claims`. The resulting bytes are
62+// (optionally) tagged with CBOR tag 61 then carry a tagged COSE_Sign1
63+// whose payload is the encoded Claims Set.
64+pub fn sign(claims ClaimsSet, key cose.Key, opts SignOptions) ![]u8 {
65+ payload := claims.encode()!
66+ cose_message := cose.sign1(payload, key,
67+ protected: opts.protected
68+ unprotected: opts.unprotected
69+ external_aad: opts.external_aad
70+ untagged: opts.untagged_cose
71+ )!
72+ return wrap_cwt(cose_message, opts.tagged_cwt)
73+}
74+
75+// verify parses, unwraps and verifies a signed CWT, returning the
76+// claims set. The outer tag 61 is accepted-but-not-required.
77+pub fn verify(token []u8, key cose.Key, opts VerifyOptions) !ClaimsSet {
78+ cose_message := unwrap_cwt(token)
79+ payload := cose.verify1(cose_message, key, external_aad: opts.external_aad)!
80+ return ClaimsSet.decode(payload)!
81+}
82+
83+// mac produces a MACed CWT (Mac0 mode) from `claims`.
84+pub fn mac(claims ClaimsSet, key cose.Key, opts MacOptions) ![]u8 {
85+ payload := claims.encode()!
86+ cose_message := cose.mac0(payload, key,
87+ protected: opts.protected
88+ unprotected: opts.unprotected
89+ external_aad: opts.external_aad
90+ untagged: opts.untagged_cose
91+ )!
92+ return wrap_cwt(cose_message, opts.tagged_cwt)
93+}
94+
95+// verify_mac parses, unwraps and verifies a MACed CWT.
96+pub fn verify_mac(token []u8, key cose.Key, opts VerifyMacOptions) !ClaimsSet {
97+ cose_message := unwrap_cwt(token)
98+ payload := cose.verify_mac0(cose_message, key, external_aad: opts.external_aad)!
99+ return ClaimsSet.decode(payload)!
100+}
101+
102+// wrap_cwt prepends the CBOR tag 61 wrapper if `tagged` is true.
103+fn wrap_cwt(cose_message []u8, tagged bool) []u8 {
104+ if !tagged {
105+ return cose_message
106+ }
107+ mut p := cbor.new_packer(cbor.EncodeOpts{})
108+ p.pack_tag(tag_cwt)
109+ mut out := p.bytes()
110+ out << cose_message
111+ return out
112+}
113+
114+// unwrap_cwt strips the optional outer tag 61 and returns the inner
115+// COSE message bytes. Tagged and untagged inputs are both accepted.
116+//
117+// Tag 61 encodes as exactly two bytes (`0xD8 0x3D`, RFC 8949 §3.4)
118+// since 61 fits in a single-octet argument; checking those two bytes
119+// directly is faster and safer than running a partial CBOR decode
120+// just to peek a tag we'd then have to roll back. Any inner COSE tag
121+// (e.g. 18 for Sign1) starts with a different byte and is left to the
122+// downstream parser.
123+fn unwrap_cwt(token []u8) []u8 {
124+ if token.len >= 2 && token[0] == 0xD8 && token[1] == 0x3D {
125+ return token[2..].clone()
126+ }
127+ return token.clone()
128+}
@@ -0,0 +1,201 @@
1+// Tests for the CWT module. Reference vectors come from RFC 8392
2+// Appendix A and the matching JSON files in the cose-wg/Examples
3+// repository (Unlicense). MAC algorithms produce deterministic tags so
4+// we can match output bytes-exactly; ECDSA-signed vectors are
5+// verified rather than reproduced.
6+module cwt
7+
8+import encoding.hex
9+import encoding.cose
10+
11+// Sample claims from RFC 8392 Appendix A.1.
12+const a1_payload_hex = 'a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b71'
13+
14+fn build_a1_claims() ClaimsSet {
15+ mut c := ClaimsSet{}
16+ c.iss = 'coap://as.example.com'
17+ c.sub = 'erikw'
18+ c.aud = ['coap://light.example.com']
19+ c.exp = 1444064944
20+ c.nbf = 1443944944
21+ c.iat = 1443944944
22+ c.cti = [u8(0x0b), 0x71]
23+ return c
24+}
25+
26+fn test_claims_set_encodes_to_rfc_appendix_a1_bytes() {
27+ c := build_a1_claims()
28+ got := c.encode()!
29+ want := hex.decode(a1_payload_hex)!
30+ assert got == want
31+}
32+
33+fn test_claims_set_decode_roundtrip() {
34+ want := build_a1_claims()
35+ encoded := want.encode()!
36+ got := ClaimsSet.decode(encoded)!
37+ assert (got.iss or { '' }) == (want.iss or { '' })
38+ assert (got.sub or { '' }) == (want.sub or { '' })
39+ assert got.aud == want.aud
40+ assert (got.exp or { 0 }) == (want.exp or { 0 })
41+ assert (got.nbf or { 0 }) == (want.nbf or { 0 })
42+ assert (got.iat or { 0 }) == (want.iat or { 0 })
43+ assert (got.cti or { []u8{} }) == (want.cti or { []u8{} })
44+}
45+
46+fn test_claims_set_decode_real_appendix_a1_payload() {
47+ got := ClaimsSet.decode(hex.decode(a1_payload_hex)!)!
48+ assert (got.iss or { '' }) == 'coap://as.example.com'
49+ assert (got.sub or { '' }) == 'erikw'
50+ assert got.aud == ['coap://light.example.com']
51+ assert (got.exp or { 0 }) == 1444064944
52+ assert (got.nbf or { 0 }) == 1443944944
53+ assert (got.iat or { 0 }) == 1443944944
54+ assert (got.cti or { []u8{} }) == [u8(0x0b), 0x71]
55+}
56+
57+// RFC 8392 Appendix A.3 — Signed CWT (ECDSA P-256). The signature is
58+// non-deterministic, so we only verify the reference message. Public
59+// key from A_3.json.
60+const a3_p256_x_hex = '143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f'
61+const a3_p256_y_hex = '60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9'
62+const a3_p256_d_hex = '6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19'
63+const a3_signed_message_hex = 'D28443A10126A05850A70175636F61703A2F2F61732E6578616D706C652E636F6D02656572696B77037818636F61703A2F2F6C696768742E6578616D706C652E636F6D041A5612AEB0051A5610D9F0061A5610D9F007420B7158405427C1FF28D23FBAD1F29C4C7C6A555E601D6FA29F9179BC3D7438BACACA5ACD08C8D4D4F96131680C429A01F85951ECEE743A52B9B63632C57209120E1C9E30'
64+
65+fn test_verify_signed_cwt_appendix_a3() {
66+ x := hex.decode(a3_p256_x_hex)!
67+ y := hex.decode(a3_p256_y_hex)!
68+ pub_key := cose.Key.ec2_public(.p_256, x, y)
69+ // The reference vector is NOT wrapped in tag 61, so verify accepts
70+ // the raw COSE message.
71+ token := hex.decode(a3_signed_message_hex)!
72+ claims := verify(token, pub_key)!
73+ assert (claims.iss or { '' }) == 'coap://as.example.com'
74+ assert (claims.sub or { '' }) == 'erikw'
75+}
76+
77+fn test_sign_and_verify_cwt_roundtrip_es256() {
78+ x := hex.decode(a3_p256_x_hex)!
79+ y := hex.decode(a3_p256_y_hex)!
80+ d := hex.decode(a3_p256_d_hex)!
81+ priv := cose.Key.ec2_private(.p_256, x, y, d)
82+ pub_key := cose.Key.ec2_public(.p_256, x, y)
83+ mut hp := cose.Headers{}
84+ hp.algorithm = .es256
85+ claims := build_a1_claims()
86+ token := sign(claims, priv, protected: hp)!
87+ got := verify(token, pub_key)!
88+ assert (got.iss or { '' }) == 'coap://as.example.com'
89+}
90+
91+// RFC 8392 Appendix A.4 — MACed CWT (HMAC 256/64). The tag is
92+// deterministic so we can match bytes-exactly.
93+const a4_key_hex = '403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388'
94+const a4_maced_message_hex = 'D18443A10104A05850A70175636F61703A2F2F61732E6578616D706C652E636F6D02656572696B77037818636F61703A2F2F6C696768742E6578616D706C652E636F6D041A5612AEB0051A5610D9F0061A5610D9F007420B7148093101EF6D789200'
95+
96+fn test_mac_cwt_matches_appendix_a4_vector() {
97+ k := hex.decode(a4_key_hex)!
98+ key := cose.Key.symmetric(k)
99+ mut hp := cose.Headers{}
100+ hp.algorithm = .hmac_256_64
101+ claims := build_a1_claims()
102+ got := mac(claims, key, protected: hp, tagged_cwt: false)!
103+ want := hex.decode(a4_maced_message_hex)!
104+ assert got == want
105+}
106+
107+fn test_verify_maced_cwt_appendix_a4() {
108+ k := hex.decode(a4_key_hex)!
109+ key := cose.Key.symmetric(k)
110+ token := hex.decode(a4_maced_message_hex)!
111+ claims := verify_mac(token, key)!
112+ assert (claims.iss or { '' }) == 'coap://as.example.com'
113+ assert (claims.cti or { []u8{} }) == [u8(0x0b), 0x71]
114+}
115+
116+fn test_cwt_outer_tag_61_roundtrip() {
117+ k := hex.decode(a4_key_hex)!
118+ key := cose.Key.symmetric(k)
119+ mut hp := cose.Headers{}
120+ hp.algorithm = .hmac_256_64
121+ claims := build_a1_claims()
122+ token := mac(claims, key, protected: hp, tagged_cwt: true)!
123+ // First two bytes must be the tag 61 wrapper (D8 3D).
124+ assert token[0] == 0xD8
125+ assert token[1] == 0x3D
126+ got := verify_mac(token, key)!
127+ assert (got.iss or { '' }) == 'coap://as.example.com'
128+}
129+
130+fn test_aud_single_string_form_on_wire() {
131+ // Single-element aud must encode as tstr, not [tstr] — RFC 7519/8392.
132+ mut c := ClaimsSet{}
133+ c.aud = ['solo']
134+ encoded := c.encode()!
135+ parsed := ClaimsSet.decode(encoded)!
136+ assert parsed.aud == ['solo']
137+ // 0xA1 (map(1)) 0x03 (label 3 = aud) 0x64 (tstr len 4) "solo"
138+ assert encoded == [u8(0xA1), 0x03, 0x64, 0x73, 0x6F, 0x6C, 0x6F]
139+}
140+
141+fn test_aud_multi_string_form_on_wire() {
142+ mut c := ClaimsSet{}
143+ c.aud = ['a', 'b']
144+ encoded := c.encode()!
145+ parsed := ClaimsSet.decode(encoded)!
146+ assert parsed.aud == ['a', 'b']
147+}
148+
149+fn test_validate_time_passes_inside_window() {
150+ c := ClaimsSet{
151+ exp: 2_000_000_000
152+ nbf: 1_000_000_000
153+ }
154+ c.validate_time(1_500_000_000)!
155+}
156+
157+fn test_validate_time_rejects_expired_token() {
158+ c := ClaimsSet{
159+ exp: 1_000_000_000
160+ }
161+ if _ := c.validate_time(2_000_000_000) {
162+ assert false, 'expired token must fail validation'
163+ } else {
164+ assert err is ClaimExpired
165+ assert err.msg().contains('expired')
166+ }
167+}
168+
169+fn test_validate_time_rejects_token_before_nbf() {
170+ c := ClaimsSet{
171+ nbf: 2_000_000_000
172+ }
173+ if _ := c.validate_time(1_000_000_000) {
174+ assert false, 'pre-nbf token must fail validation'
175+ } else {
176+ assert err is ClaimNotYetValid
177+ assert err.msg().contains('not yet valid')
178+ }
179+}
180+
181+fn test_validate_time_passes_when_neither_claim_set() {
182+ c := ClaimsSet{}
183+ c.validate_time(1_500_000_000)!
184+}
185+
186+fn test_expired_helper_returns_false_without_exp() {
187+ c := ClaimsSet{}
188+ assert !c.expired(9_999_999_999)
189+}
190+
191+fn test_extra_int_claim_preserved_on_roundtrip() {
192+ mut c := ClaimsSet{}
193+ c.extra_int_claims << ClaimEntry{
194+ label: 100
195+ value: cose.Headers{}.to_value() // any cbor.Value works; use empty map
196+ }
197+ encoded := c.encode()!
198+ parsed := ClaimsSet.decode(encoded)!
199+ assert parsed.extra_int_claims.len == 1
200+ assert parsed.extra_int_claims[0].label == 100
201+}
@@ -0,0 +1,28 @@
1+{
2+ "title":"CWT - Appendix A.3 - Signed w/ ECDSA 256",
3+ "input":{
4+ "plaintext_hex":"a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b71",
5+ "sign0":{
6+ "key":{
7+ "kty":"EC",
8+ "crv":"P-256",
9+ "d_hex":"6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19",
10+ "y_hex":"60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9",
11+ "x_hex":"143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f"
12+ },
13+ "unprotected":{
14+ },
15+ "protected":{
16+ "alg":"ES256"
17+ },
18+ "alg":"ES256"
19+ }
20+ },
21+ "intermediates":{
22+ "ToBeSign_hex":"846A5369676E61747572653143A10126405850A70175636F61703A2F2F61732E6578616D706C652E636F6D02656572696B77037818636F61703A2F2F6C696768742E6578616D706C652E636F6D041A5612AEB0051A5610D9F0061A5610D9F007420B71"
23+ },
24+ "output":{
25+ "cbor_diag":"18([h'A10126', {}, h'A70175636F61703A2F2F61732E6578616D706C652E636F6D02656572696B77037818636F61703A2F2F6C696768742E6578616D706C652E636F6D041A5612AEB0051A5610D9F0061A5610D9F007420B71', h'5427C1FF28D23FBAD1F29C4C7C6A555E601D6FA29F9179BC3D7438BACACA5ACD08C8D4D4F96131680C429A01F85951ECEE743A52B9B63632C57209120E1C9E30'])",
26+ "cbor":"D28443A10126A05850A70175636F61703A2F2F61732E6578616D706C652E636F6D02656572696B77037818636F61703A2F2F6C696768742E6578616D706C652E636F6D041A5612AEB0051A5610D9F0061A5610D9F007420B7158405427C1FF28D23FBAD1F29C4C7C6A555E601D6FA29F9179BC3D7438BACACA5ACD08C8D4D4F96131680C429A01F85951ECEE743A52B9B63632C57209120E1C9E30"
27+ }
28+}
@@ -0,0 +1,38 @@
1+{
2+ "title":"CWT doument - Appendix A.4",
3+ "input":{
4+ "plaintext_hex":"a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b71",
5+ "mac0":{
6+ "alg":"HS256/64",
7+ "protected":{
8+ "alg":"HS256/64"
9+ },
10+ "recipients":[
11+ {
12+ "unprotected":{
13+ "alg":"direct",
14+ "kid":"our-secret"
15+ },
16+ "key":{
17+ "kty":"oct",
18+ "kid":"our-secret",
19+ "use":"enc",
20+ "k_hex":"403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388"
21+ }
22+ }
23+ ]
24+ }
25+ },
26+ "intermediates":{
27+ "ToMac_hex":"84644D41433043A10104405850A70175636F61703A2F2F61732E6578616D706C652E636F6D02656572696B77037818636F61703A2F2F6C696768742E6578616D706C652E636F6D041A5612AEB0051A5610D9F0061A5610D9F007420B71",
28+ "CEK_hex":"403697DE87AF64611C1D32A05DAB0FE1FCB715A86AB435F1EC99192D79569388",
29+ "recipients":[
30+ {
31+ }
32+ ]
33+ },
34+ "output":{
35+ "cbor_diag":"17([h'A10104', {}, h'A70175636F61703A2F2F61732E6578616D706C652E636F6D02656572696B77037818636F61703A2F2F6C696768742E6578616D706C652E636F6D041A5612AEB0051A5610D9F0061A5610D9F007420B71', h'093101EF6D789200'])",
36+ "cbor":"D18443A10104A05850A70175636F61703A2F2F61732E6578616D706C652E636F6D02656572696B77037818636F61703A2F2F6C696768742E6578616D706C652E636F6D041A5612AEB0051A5610D9F0061A5610D9F007420B7148093101EF6D789200"
37+ }
38+}
@@ -0,0 +1,39 @@
1+{
2+ "title":"CWT draft - Example A.5 - Encrypted",
3+ "input":{
4+ "plaintext_hex":"a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b71",
5+ "encrypted":{
6+ "protected":{
7+ "alg":"AES-CCM-16-128/64"
8+ },
9+ "recipients":[
10+ {
11+ "key":{
12+ "kty":"oct",
13+ "kid":"our-secret",
14+ "use":"enc",
15+ "k_hex":"231f4c4d4d3051fdc2ec0a3851d5b383"
16+ },
17+ "unprotected":{
18+ "alg":"direct"
19+ }
20+ }
21+ ]
22+ },
23+ "rng_stream":[
24+ "99A0D7846E762C49FFE8A63E0B"
25+ ]
26+ },
27+ "intermediates":{
28+ "recipients":[
29+ {
30+ }
31+ ],
32+ "AAD_hex":"8368456E63727970743043A1010A40",
33+ "CEK_hex":"231F4C4D4D3051FDC2EC0A3851D5B383"
34+ },
35+ "output":{
36+ "cbor_diag":"16([h'A1010A', {5: h'99A0D7846E762C49FFE8A63E0B'}, h'B918A11FD81E438B7F973D9E2E119BCB22424BA0F38A80F27562F400EE1D0D6C0FDB559C02421FD384FC2EBE22D7071378B0EA7428FFF157444D45F7E6AFCDA1AAE5F6495830C58627087FC5B4974F319A8707A635DD643B'])",
37+ "cbor":"D08343A1010AA1054D99A0D7846E762C49FFE8A63E0B5858B918A11FD81E438B7F973D9E2E119BCB22424BA0F38A80F27562F400EE1D0D6C0FDB559C02421FD384FC2EBE22D7071378B0EA7428FFF157444D45F7E6AFCDA1AAE5F6495830C58627087FC5B4974F319A8707A635DD643B"
38+ }
39+}
@@ -0,0 +1,39 @@
1+{
2+ "title":"CWT draft - Example A.6 - Signed & Encrypted",
3+ "input":{
4+ "plaintext_hex":"d28443a10126a05850a70175636f61703a2f2f61732e6578616d706c652e636f6d02656572696b77037818636f61703a2f2f6c696768742e6578616d706c652e636f6d041a5612aeb0051a5610d9f0061a5610d9f007420b7158405427c1ff28d23fbad1f29c4c7c6a555e601d6fa29f9179bc3d7438bacaca5acd08c8d4d4f96131680c429a01f85951ecee743a52b9b63632c57209120e1c9e30",
5+ "encrypted":{
6+ "protected":{
7+ "alg":"AES-CCM-16-128/64"
8+ },
9+ "recipients":[
10+ {
11+ "key":{
12+ "kty":"oct",
13+ "kid":"our-secret",
14+ "use":"enc",
15+ "k_hex":"231f4c4d4d3051fdc2ec0a3851d5b383"
16+ },
17+ "unprotected":{
18+ "alg":"direct"
19+ }
20+ }
21+ ]
22+ },
23+ "rng_stream":[
24+ "86BBD41CC32604396324B7F380"
25+ ]
26+ },
27+ "intermediates":{
28+ "recipients":[
29+ {
30+ }
31+ ],
32+ "AAD_hex":"8368456E63727970743043A1010A40",
33+ "CEK_hex":"231F4C4D4D3051FDC2EC0A3851D5B383"
34+ },
35+ "output":{
36+ "cbor_diag":"16([h'A1010A', {5: h'86BBD41CC32604396324B7F380'}, h'72439FBFF538AA7B601EBFB29454050A3C99FD13B27216D084556496C7355C4BB462510F8E0E8479DBE08722D620E96BCB7764D75140D96220F062679B46B897E7ABE0C325DC2C96D8BB2C8334E3B92A42C0078983E753C054E647AD5387ED149F802F52B5A95EBF5F153C4FD64854AB7531E082B7F22721F939D257C94F8BC248E1D9CF04F9DD4E5DE7AB62DF37842FABEC230A657D4ABF7162BC786345EBB8EB3AF0'])",
37+ "cbor":"D08343A1010AA1054D86BBD41CC32604396324B7F38058A372439FBFF538AA7B601EBFB29454050A3C99FD13B27216D084556496C7355C4BB462510F8E0E8479DBE08722D620E96BCB7764D75140D96220F062679B46B897E7ABE0C325DC2C96D8BB2C8334E3B92A42C0078983E753C054E647AD5387ED149F802F52B5A95EBF5F153C4FD64854AB7531E082B7F22721F939D257C94F8BC248E1D9CF04F9DD4E5DE7AB62DF37842FABEC230A657D4ABF7162BC786345EBB8EB3AF0"
38+ }
39+}
@@ -0,0 +1,38 @@
1+{
2+ "title":"CWT doument - Appendix A.7",
3+ "input":{
4+ "plaintext_hex":"a106fb41d584367c200000",
5+ "mac0":{
6+ "alg":"HS256/64",
7+ "protected":{
8+ "alg":"HS256/64"
9+ },
10+ "recipients":[
11+ {
12+ "unprotected":{
13+ "alg":"direct",
14+ "kid":"our-secret"
15+ },
16+ "key":{
17+ "kty":"oct",
18+ "kid":"our-secret",
19+ "use":"enc",
20+ "k_hex":"403697de87af64611c1d32a05dab0fe1fcb715a86ab435f1ec99192d79569388"
21+ }
22+ }
23+ ]
24+ }
25+ },
26+ "intermediates":{
27+ "ToMac_hex":"84644D41433043A10104404BA106FB41D584367C200000",
28+ "CEK_hex":"403697DE87AF64611C1D32A05DAB0FE1FCB715A86AB435F1EC99192D79569388",
29+ "recipients":[
30+ {
31+ }
32+ ]
33+ },
34+ "output":{
35+ "cbor_diag":"17([h'A10104', {}, h'A106FB41D584367C200000', h'B8816F34C0542892'])",
36+ "cbor":"D18443A10104A04BA106FB41D584367C20000048B8816F34C0542892"
37+ }
38+}