encoding.cwt #
encoding.cwt
CBOR Web Tokens — pure-V implementation of RFC 8392, the CBOR analogue of JSON Web Tokens. A CWT is a Claims Set encoded as CBOR and wrapped in a COSE message (COSE_Sign1 or COSE_Mac0 in the current set of supported wrappers).
This module is a thin layer over encoding.cose — all the cryptography is handled there. CWT-specific concerns are:
- Modelling the standard claims (
iss,sub,aud,exp,nbf,iat,cti) as typed fields onClaimsSet. - Serialising/parsing the CBOR claims map.
- Adding/removing the optional outer CBOR tag 61 (
tag_cwt).
Quick examples
Sign a CWT (ES256)
import encoding.cwt
import encoding.cose
import encoding.hex
fn main() {
x := hex.decode('143329cce7868e416927599cf65a34f3ce2ffda55a7eca69ed8919a394d42f0f')!
y := hex.decode('60f7f1a780d8a783bfb7a2dd6b2796e8128dbbcef9d3d168db9529971a36e7b9')!
d := hex.decode('6c1382765aec5358f117733d281c1c7bdc39884d04a45a1e6c67c858bc206c19')!
priv := cose.Key.ec2_private(.p_256, x, y, d)
pub_key := cose.Key.ec2_public(.p_256, x, y)
claims := cwt.ClaimsSet{
iss: 'coap://as.example.com'
sub: 'erikw'
aud: ['coap://light.example.com']
exp: 1444064944
iat: 1443944944
}
token := cwt.sign(claims, priv,
protected: cose.Headers{
algorithm: .es256
}
)!
got := cwt.verify(token, pub_key)!
assert (got.iss or { '' }) == 'coap://as.example.com'
}
Verify a CWT and check expiry
cwt.verify only authenticates the token; the exp / nbf window must be enforced by the application. Use validate_time right after verification:
import time
claims := cwt.verify(token, pub_key)!
claims.validate_time(time.now().unix) or {
if err is cwt.ClaimExpired { return error('token expired') }
if err is cwt.ClaimNotYetValid { return error('token not yet valid') }
return err
}
MAC a CWT (HMAC 256/64)
key := cose.Key.symmetric(secret_bytes)
token := cwt.mac(claims, key, protected: cose.Headers{ algorithm: .hmac_256_64 })!
got := cwt.verify_mac(token, key)!
Outer tag 61
The default for sign and mac is to wrap the COSE message in CBOR tag 61 (the CWT marker, RFC 8392 §6). Set tagged_cwt: false in options to emit just the inner COSE message — handy for contexts where the surrounding protocol already disambiguates the payload type. Verification accepts both forms.
Standard claims modelled
| Claim | Label | V field | Type |
|---|---|---|---|
| iss | 1 | iss |
?string |
| sub | 2 | sub |
?string |
| aud | 3 | aud |
[]string (single = string form on the wire) |
| exp | 4 | exp |
?i64 Unix seconds |
| nbf | 5 | nbf |
?i64 |
| iat | 6 | iat |
?i64 |
| cti | 7 | cti |
?[]u8 |
Application-specific claims go into extra_int_claims / extra_text_claims, with raw cbor.Value payloads. Labels 1-7 stay reserved for the typed fields above: encode() rejects them in extra_int_claims rather than emit a payload ClaimsSet.decode() would read back differently.
Time handling
exp, nbf and iat are decoded into i64 Unix seconds. Encoded output uses the integer form of NumericDate (RFC 8392 §3). Integral CBOR floats are accepted on decode; fractional values are rejected because the claims model stores only whole seconds.
validate_time(now_unix i64) does the standard nbf ≤ now < exp check and returns a ClaimExpired or ClaimNotYetValid typed error. The check is not run automatically by verify / verify_mac so that callers stay in control of the clock source (real time, fixed test time, replay-safe context, etc.).
For one-off boolean checks: claims.expired(now_unix) and claims.not_yet_valid(now_unix).
See also
encoding.cose— the underlying signature/MAC primitives.encoding.cbor— the CBOR codec.
Constants #
const tag_cwt = u64(61)
CBOR tag 61 (RFC 8392 §6) marks a CWT, distinguishing it from a generic COSE message.
fn mac #
fn mac(claims ClaimsSet, key cose.Key, opts MacOptions) ![]u8
mac produces a MACed CWT (Mac0 mode) from claims.
fn sign #
fn sign(claims ClaimsSet, key cose.Key, opts SignOptions) ![]u8
sign produces a signed CWT from claims. The resulting bytes are (optionally) tagged with CBOR tag 61 then carry a tagged COSE_Sign1 whose payload is the encoded Claims Set.
fn verify #
fn verify(token []u8, key cose.Key, opts VerifyOptions) !ClaimsSet
verify parses, unwraps and verifies a signed CWT, returning the claims set. The outer tag 61 is accepted-but-not-required.
fn verify_mac #
fn verify_mac(token []u8, key cose.Key, opts VerifyMacOptions) !ClaimsSet
verify_mac parses, unwraps and verifies a MACed CWT.
fn ClaimsSet.decode #
fn ClaimsSet.decode(data []u8) !ClaimsSet
ClaimsSet.decode parses a CBOR-encoded Claims Set. Unknown claims are kept in extra_int_claims / extra_text_claims.
struct ClaimEntry #
struct ClaimEntry {
pub:
label i64
value cbor.Value
}
ClaimEntry is one (int label, value) pair.
struct ClaimExpired #
struct ClaimExpired {
Error
pub:
exp i64
now i64
}
ClaimExpired is returned by validate_time when now >= exp.
fn (ClaimExpired) msg #
fn (e &ClaimExpired) msg() string
msg formats a ClaimExpired for IError.msg().
struct ClaimNotYetValid #
struct ClaimNotYetValid {
Error
pub:
nbf i64
now i64
}
ClaimNotYetValid is returned by validate_time when now < nbf.
fn (ClaimNotYetValid) msg #
fn (e &ClaimNotYetValid) msg() string
msg formats a ClaimNotYetValid for IError.msg().
struct ClaimsSet #
struct ClaimsSet {
pub mut:
iss ?string
sub ?string
// aud — RFC 8392 requires a single text string. The slice is empty
// when absent and has exactly one element when present.
aud []string
exp ?i64
nbf ?i64
iat ?i64
cti ?[]u8
// extra_int_claims carries unmodelled integer-labelled claims.
extra_int_claims []ClaimEntry
// extra_text_claims carries unmodelled text-labelled claims.
extra_text_claims []TextClaimEntry
}
ClaimsSet is the V representation of a CWT Claims Set (RFC 8392 §3). Time-valued claims (exp, nbf, iat) are stored as whole Unix seconds (i64). RFC 8392 also allows fractional NumericDate via CBOR floats; the decoder accepts only floats that represent whole seconds.
fn (ClaimsSet) encode #
fn (c ClaimsSet) encode() ![]u8
encode returns the canonical CBOR encoding of the claims set as a CBOR map. The output is the bytes that go into the payload slot of the surrounding COSE message.
fn (ClaimsSet) expired #
fn (c ClaimsSet) expired(now_unix i64) bool
expired reports whether the claims set's exp (expiration time) is at or before now_unix. Returns false when exp is absent — per RFC 8392 §3.1.4 a CWT without exp does not expire on its own.
Pass time.now().unix for the wall-clock check; pass a fixed timestamp for unit tests or replay-safe contexts.
fn (ClaimsSet) not_yet_valid #
fn (c ClaimsSet) not_yet_valid(now_unix i64) bool
not_yet_valid reports whether the claims set's nbf (not-before) is in the future relative to now_unix. Returns false when nbf is absent.
fn (ClaimsSet) validate_time #
fn (c ClaimsSet) validate_time(now_unix i64) !
validate_time runs the nbf/exp checks against now_unix and returns a typed error when the token is outside its validity window, or none when both checks pass (or the relevant claims are absent). This is the convenience helper most application code wants right after cwt.verify(...).
struct MacOptions #
struct MacOptions {
pub:
protected cose.Headers
unprotected cose.Headers
external_aad []u8
untagged_cose bool
tagged_cwt bool = true
}
MacOptions controls the MACing of a CWT (Mac0 mode).
struct SignOptions #
struct SignOptions {
pub:
protected cose.Headers
unprotected cose.Headers
external_aad []u8
// untagged_cose disables the inner COSE tag 18 wrapper. The default
// (tagged) is what every interop test in RFC 8392 uses.
untagged_cose bool
// tagged_cwt wraps the COSE message in CBOR tag 61. The default is
// `true`. RFC 8392 §6 RECOMMENDS the tag for self-describing
// payloads but allows omitting it when the context already
// disambiguates.
tagged_cwt bool = true
}
SignOptions controls the signing of a CWT. The same fields as cose.Sign1Options plus a CWT-specific tagged_cwt flag controlling the outer tag 61 wrapper.
struct TextClaimEntry #
struct TextClaimEntry {
pub:
label string
value cbor.Value
}
TextClaimEntry is one (text label, value) pair.
struct VerifyMacOptions #
struct VerifyMacOptions {
pub:
external_aad []u8
}
VerifyMacOptions mirrors MacOptions for the verification side.
struct VerifyOptions #
struct VerifyOptions {
pub:
external_aad []u8
}
VerifyOptions mirrors SignOptions for the verification side.