Skip to content

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

Open
davlgd wants to merge 2 commits into
vlang:masterfrom
davlgd:davlgd-cose-cwt
Open

encoding.cose: add COSE (RFC 9052) and CWT (RFC 8392) modules#27111
davlgd wants to merge 2 commits into
vlang:masterfrom
davlgd:davlgd-cose-cwt

Conversation

@davlgd
Copy link
Copy Markdown
Contributor

@davlgd davlgd commented May 8, 2026

This PR adds two complementary modules to the standard library:

  • encoding.cose — CBOR Object Signing and Encryption, signing + MAC subset of RFC 9052 and RFC 9053.
  • encoding.cwt — CBOR Web Tokens (RFC 8392) built on top of cose.

Both modules sit alongside encoding.cbor (merged in #27018) and use its codec under the hood. No new C code, no new third-party crypto: everything is built on the primitives already in vlib/crypto (ecdsa, ed25519, hmac, sha256, sha512).

Why

CBOR is everywhere: WebAuthn / FIDO2 attestations, IETF SUIT (firmware updates over LWM2M), Matter (smart home), CoAP / OSCORE, EDHOC, the European Digital Identity Wallet… All of them serialise their tokens as COSE or CWT.

What's covered

Message type Tag Status
COSE_Sign1 18
COSE_Sign 98
COSE_Mac0 17
COSE_Mac 97 ✅ (direct mode)
Algorithm IANA Status
ES256 -7
ES384 -35
ES512 -36 ✅ (P-521 + SHA-512)
EdDSA -8 ✅ Ed25519
HMAC 256/64 4
HMAC 256/256 5
HMAC 384/384 6
HMAC 512/512 7

CWT models the seven RFC 8392 §3 standard claims (iss, sub, aud, exp, nbf, iat, cti) plus pass-through for application claims, and handles the optional outer CBOR tag 61 wrapper.

Module surface

import encoding.cose
import encoding.cwt
// Sign and verify a payload (Sign1 / EdDSA).
signed := cose.sign1('payload'.bytes(), priv_key, protected: hp)!
got    := cose.verify1(signed, pub_key)!
// MAC a payload (Mac0 / HMAC-SHA256).
maced := cose.mac0('payload'.bytes(), sym_key, protected: hp)!
got2  := cose.verify_mac0(maced, sym_key)!
// Sign and verify a CBOR Web Token.
token := cwt.sign(claims, priv_key, protected: hp)!
back  := cwt.verify(token, pub_key)!

A complete example program lives at examples/cose_cwt.v.

Conformance / test vectors

The cose-wg/Examples repository is the canonical interop corpus. Its relevant fixtures are vendored under vlib/encoding/cose/tests/cose_wg/ and vlib/encoding/cwt/tests/rfc8392/, and run as part of v test. For deterministic algorithms (EdDSA, all HMAC variants) the V output is validated byte-for-byte; for ECDSA (randomised signatures) each reference message is verified. The negative *-fail-* vectors exercise the verifier's rejection paths against externally produced bad messages.

Vector Type Algorithm Mode
ecdsa-sig-01 Sign1 ES256 verify
ecdsa-sig-02 Sign1 ES384 verify + roundtrip
ecdsa-sig-03 Sign1 ES512 / P-521 verify + roundtrip
eddsa-sig-01 Sign1 EdDSA bytes-exact
eddsa-01 Sign EdDSA verify (multi-signer wrapper)
sign1-fail-01..04 Sign1 ES256 reject (bad tag, tampered payload, replaced alg)
sign1-pass-02 Sign1 ES256 verify with external AAD
HMac-01 Mac HS256 verify (single direct recipient)
HMac-04 Mac HS256 reject (corrupted tag)
HMac-05 Mac HS256/64 verify
HMac-enc-01 Mac0 HS256 bytes-exact
HMac-enc-02 Mac0 HS384 bytes-exact
HMac-enc-03 Mac0 HS512 bytes-exact
HMac-enc-05 Mac0 HS256/64 bytes-exact
RFC 8392 A.3 CWT (Sign1) ES256 verify
RFC 8392 A.4 CWT (Mac0) HS256/64 bytes-exact

In addition to the public corpus:

  • ec_signature_test.v — DER ↔ R‖S conversion edge cases (leading zeros, MSB-set sign disambiguation, P-521 long-form lengths).
  • structures_test.vSig_structure / MAC_structure byte output matches RFC 9052 §4.4 / §6.3.
  • headers_test.v — canonical key sorting (RFC 8949 §4.2.1), empty-protected-as-zero-length-bstr rule, unknown-label preservation.
  • sign1_test.v / sign_test.v / mac0_test.v / mac_test.v — sign/verify roundtrip per algorithm; tampered-payload and wrong-key rejection paths; detached-payload and external-AAD modes; sanity caps on signer/recipient array counts; unknown crit label rejection per RFC 9052 §3.1.
  • cwt_test.v — Claims Set encoding, the aud tstr-vs-array form rule from RFC 7519/8392, optional tag-61 wrapper, RFC 8392 A.3 and A.4 reproduction, nbf/exp validity-window helper.

I've also tested these modules against lib in other languages to check interop.

Out of scope (deliberate)

  • AEAD encryption (COSE_Encrypt0 / COSE_Encrypt) — needs AES-GCM, AES-CCM, ChaCha20-Poly1305 in vlib/crypto, which is a separate effort. Adding the message types later is purely additive.
  • RSA signatures — RFC 9053 lists PS256/384/512 and RS256/384/512 (legacy). Adding them is mechanical if vlib/crypto ships RSA-PSS, but the demand is small relative to ECDSA + EdDSA.
  • Key agreement / wrap modes (ECDH-ES, A128KW…) — these belong with the encryption work since they're meaningful only when there is something to encrypt.
  • Compressed EC points in COSE_Key.
  • Counter signatures (RFC 9338).

@medvednikov medvednikov reopened this May 23, 2026
@medvednikov
Copy link
Copy Markdown
Member

@codex review

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 60bfca67ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +323 to +327
fn check_critical(h Headers) ! {
for label in h.critical {
if label !in [label_alg, label_crit, label_content_type, label_kid, label_iv,
label_partial_iv] {
return MalformedMessage{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Enforce protected-only semantics for crit labels

check_critical only validates that each entry is a known integer label, but it never enforces RFC 9052's fatal conditions that crit must be in the protected bucket and that every listed label must also be present there. Because verification paths call this helper only on protected headers, a message can place crit (or the referenced header) in unprotected headers and still verify, which allows required-to-understand semantics to be bypassed.

Useful? React with 👍 / 👎.

Comment on lines +222 to +224
if alg := algorithm_from_int(code) {
h.algorithm = alg
} else {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject duplicate header labels during header-map decode

Header-map decoding does not track already-seen labels, so repeated labels are silently accepted and later values overwrite earlier ones (for example, duplicate alg entries repeatedly assign h.algorithm). RFC 9052 requires duplicate labels to be treated as malformed; accepting them can create parser ambiguity and cross-implementation verification differences on crafted inputs.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants