medvednikov

/

vqPublic
0 commits21 issue12 pull requests0 contributorsDiscussionsProjectsCI

encoding.cose: add COSE (RFC 9052) and CWT (RFC 8392) modules #18

Opendavlgdwants to mergepr/27111intomaster· last May 8
53 files changed+5322-0
examples/cose_cwt.vnew file+83-0

@@ -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+}

vlib/encoding/cose/README.mdnew file+188-0

@@ -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.

vlib/encoding/cose/algorithms.vnew file+68-0

@@ -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+}

vlib/encoding/cose/ec_signature.vnew file+182-0

@@ -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+}

vlib/encoding/cose/ec_signature_test.vnew file+136-0

@@ -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+}

vlib/encoding/cose/errors.vnew file+69-0

@@ -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+}

vlib/encoding/cose/headers.vnew file+411-0

@@ -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+}

vlib/encoding/cose/headers_test.vnew file+132-0

@@ -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+}

vlib/encoding/cose/key.vnew file+423-0

@@ -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+}

vlib/encoding/cose/mac.vnew file+247-0

@@ -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+}

vlib/encoding/cose/mac0.vnew file+157-0

@@ -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+}

vlib/encoding/cose/mac0_test.vnew file+135-0

@@ -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+}

vlib/encoding/cose/mac_test.vnew file+90-0

@@ -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+}

vlib/encoding/cose/macing.vnew file+69-0

@@ -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+}

vlib/encoding/cose/multi_curve_test.vnew file+67-0

@@ -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+}

vlib/encoding/cose/sign.vnew file+231-0

@@ -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+}

vlib/encoding/cose/sign1.vnew file+184-0

@@ -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+}

vlib/encoding/cose/sign1_test.vnew file+239-0

@@ -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+}

vlib/encoding/cose/sign_test.vnew file+110-0

@@ -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+}

vlib/encoding/cose/signing.vnew file+272-0

@@ -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+}

vlib/encoding/cose/structures.vnew file+86-0

@@ -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+}

vlib/encoding/cose/structures_test.vnew file+77-0

@@ -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+}

vlib/encoding/cose/tags.vnew file+10-0

@@ -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

vlib/encoding/cose/tests/cose_wg/HMac-01.jsonnew file+38-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/HMac-04.jsonnew file+42-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/HMac-05.jsonnew file+38-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/HMac-enc-01.jsonnew file+38-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/HMac-enc-02.jsonnew file+38-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/HMac-enc-03.jsonnew file+38-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/HMac-enc-04.jsonnew file+42-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/HMac-enc-05.jsonnew file+38-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/ecdsa-sig-01.jsonnew file+32-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/ecdsa-sig-03.jsonnew file+32-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/ecdsa-sig-04.jsonnew file+31-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/eddsa-01.jsonnew file+40-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/eddsa-02.jsonnew file+37-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/eddsa-sig-01.jsonnew file+31-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/eddsa-sig-02.jsonnew file+30-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/sign-fail-01.jsonnew file+35-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/sign-fail-02.jsonnew file+35-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/sign-fail-03.jsonnew file+37-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/sign-fail-04.jsonnew file+37-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/sign-pass-01.jsonnew file+32-0

@@ -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+}

vlib/encoding/cose/tests/cose_wg/sign-pass-02.jsonnew file+32-0

@@ -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+}

vlib/encoding/cwt/README.mdnew file+119-0

@@ -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.

vlib/encoding/cwt/claims.vnew file+273-0

@@ -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+}

vlib/encoding/cwt/cwt.vnew file+128-0

@@ -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+}

vlib/encoding/cwt/cwt_test.vnew file+201-0

@@ -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+}

vlib/encoding/cwt/tests/rfc8392/A_3.jsonnew file+28-0

@@ -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+}

vlib/encoding/cwt/tests/rfc8392/A_4.jsonnew file+38-0

@@ -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+}

vlib/encoding/cwt/tests/rfc8392/A_5.jsonnew file+39-0

@@ -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+}

vlib/encoding/cwt/tests/rfc8392/A_6.jsonnew file+39-0

@@ -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+}

vlib/encoding/cwt/tests/rfc8392/A_7.jsonnew file+38-0

@@ -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+}