Skip to main content

darkbio_crypto/cwt/claims/
mod.rs

1// crypto-rs: cryptography primitives and wrappers
2// Copyright 2026 Dark Bio AG. All rights reserved.
3//
4// Use of this source code is governed by a BSD-style
5// license that can be found in the LICENSE file.
6
7//! Standard CWT claim types.
8//!
9//! <https://datatracker.ietf.org/doc/html/rfc8392>
10
11pub mod eat;
12
13use crate::cbor::{
14    self, Cbor, Decode, Encode, MapDecode, MapEncode, MapEncodeBuffer, MapEntryAccess,
15};
16use crate::cose;
17use crate::xdsa;
18use crate::xhpke;
19
20/// The principal that issued the token (key 1).
21#[derive(Clone, Debug, PartialEq, Eq, Cbor)]
22pub struct Issuer {
23    /// Issuer identity, a URI or any string the ecosystem agrees on.
24    #[cbor(key = 1)]
25    pub iss: String,
26}
27
28/// The principal that is the subject of the token (key 2).
29#[derive(Clone, Debug, PartialEq, Eq, Cbor)]
30pub struct Subject {
31    /// Subject identity, for a device its serial or attestation subject.
32    #[cbor(key = 2)]
33    pub sub: String,
34}
35
36/// The recipients the token is intended for (key 3).
37#[derive(Clone, Debug, PartialEq, Eq, Cbor)]
38pub struct Audience {
39    /// Intended audience, a URI or any string the ecosystem agrees on.
40    #[cbor(key = 3)]
41    pub aud: String,
42}
43
44/// The time on or after which the token must not be accepted (key 4).
45#[derive(Clone, Debug, PartialEq, Eq, Cbor)]
46pub struct Expiration {
47    /// Expiration time, seconds since the Unix epoch. Rejected at or after.
48    #[cbor(key = 4)]
49    pub exp: u64,
50}
51
52/// The time before which the token must not be accepted (key 5).
53#[derive(Clone, Debug, PartialEq, Eq, Cbor)]
54pub struct NotBefore {
55    /// Not-before time, seconds since the Unix epoch. Rejected before.
56    #[cbor(key = 5)]
57    pub nbf: u64,
58}
59
60/// The time at which the token was issued (key 6).
61#[derive(Clone, Debug, PartialEq, Eq, Cbor)]
62pub struct IssuedAt {
63    /// Issue time, seconds since the Unix epoch.
64    #[cbor(key = 6)]
65    pub iat: u64,
66}
67
68/// A unique identifier for the token (key 7).
69#[derive(Clone, Debug, PartialEq, Eq, Cbor)]
70pub struct TokenId {
71    /// Token identifier, opaque bytes unique per token.
72    #[cbor(key = 7)]
73    pub cti: Vec<u8>,
74}
75
76/// Sealed trait implementation module. The supertrait lives in a private module
77/// so external crates cannot implement `ConfirmKey`.
78mod sealed {
79    use crate::cbor;
80
81    pub trait ConfirmKeySealed {
82        const ALGORITHM: i64;
83        fn to_confirm_bytes(&self) -> Vec<u8>;
84        fn from_confirm_bytes(bytes: &[u8]) -> Result<Self, cbor::Error>
85        where
86            Self: Sized;
87    }
88}
89
90/// Marker trait for key types usable with [`Confirm`].
91///
92/// Implemented for [`xdsa::PublicKey`] and [`xhpke::PublicKey`].
93pub trait ConfirmKey: sealed::ConfirmKeySealed + Clone + std::fmt::Debug {}
94
95impl sealed::ConfirmKeySealed for xdsa::PublicKey {
96    const ALGORITHM: i64 = cose::ALGORITHM_ID_XDSA;
97
98    fn to_confirm_bytes(&self) -> Vec<u8> {
99        self.to_bytes().to_vec()
100    }
101
102    fn from_confirm_bytes(bytes: &[u8]) -> Result<Self, cbor::Error> {
103        let arr: [u8; xdsa::PUBLIC_KEY_SIZE] = bytes.try_into().map_err(|_| {
104            cbor::Error::DecodeFailed(format!(
105                "cnf: unexpected key size {}, want {}",
106                bytes.len(),
107                xdsa::PUBLIC_KEY_SIZE,
108            ))
109        })?;
110        xdsa::PublicKey::from_bytes(&arr).map_err(|e| cbor::Error::DecodeFailed(e.to_string()))
111    }
112}
113
114impl ConfirmKey for xdsa::PublicKey {}
115
116impl sealed::ConfirmKeySealed for xhpke::PublicKey {
117    const ALGORITHM: i64 = cose::ALGORITHM_ID_XHPKE;
118
119    fn to_confirm_bytes(&self) -> Vec<u8> {
120        self.to_bytes().to_vec()
121    }
122
123    fn from_confirm_bytes(bytes: &[u8]) -> Result<Self, cbor::Error> {
124        let arr: [u8; xhpke::PUBLIC_KEY_SIZE] = bytes.try_into().map_err(|_| {
125            cbor::Error::DecodeFailed(format!(
126                "cnf: unexpected key size {}, want {}",
127                bytes.len(),
128                xhpke::PUBLIC_KEY_SIZE,
129            ))
130        })?;
131        xhpke::PublicKey::from_bytes(&arr).map_err(|e| cbor::Error::DecodeFailed(e.to_string()))
132    }
133}
134
135impl ConfirmKey for xhpke::PublicKey {}
136
137/// The public key bound to the token through the cnf claim (key 8, RFC 8747).
138/// The COSE_Key wrapping is handled internally.
139///
140/// A verified token authenticates this key binding, but does not prove that
141/// the presenter possesses the corresponding private key. Applications must
142/// check that separately using their protocol's proof-of-possession mechanism.
143#[derive(Clone, Debug)]
144pub struct Confirm<T: ConfirmKey> {
145    key: T,
146}
147
148impl<T: ConfirmKey> Confirm<T> {
149    /// Creates a Confirm value binding the given public key.
150    pub fn new(key: T) -> Self {
151        Self { key }
152    }
153
154    /// Returns a reference to the bound public key.
155    pub fn key(&self) -> &T {
156        &self.key
157    }
158}
159
160/// Internal: `{ 1: COSE_Key }`, the cnf claim value envelope.
161#[derive(Clone, Debug, PartialEq, Eq, Cbor)]
162struct CnfMap {
163    #[cbor(key = 1)]
164    cose_key: CoseKey,
165}
166
167/// Internal: `{ 1: kty, -2: x }`, a minimal COSE_Key. Parameter -2 carries
168/// the full public key bytes, following the OKP convention (RFC 9053 Section 7.2).
169#[derive(Clone, Debug, PartialEq, Eq, Cbor)]
170struct CoseKey {
171    #[cbor(key = 1)]
172    kty: i64,
173    #[cbor(key = -2)]
174    x: Vec<u8>,
175}
176
177impl<T: ConfirmKey> Encode for Confirm<T> {
178    fn encode_cbor_to(&self, buf: &mut Vec<u8>) -> Result<(), cbor::Error> {
179        let mut enc = MapEncodeBuffer::new(1);
180        <Self as MapEncode>::encode_map(self, &mut enc)?;
181        enc.finish_to(buf)
182    }
183}
184
185impl<T: ConfirmKey> Decode for Confirm<T> {
186    fn decode_cbor(data: &[u8]) -> Result<Self, cbor::Error> {
187        let mut dec = cbor::Decoder::new(data);
188        let result = Self::decode_cbor_notrail(&mut dec)?;
189        dec.finish()?;
190        Ok(result)
191    }
192
193    fn decode_cbor_notrail(dec: &mut cbor::Decoder<'_>) -> Result<Self, cbor::Error> {
194        let entries = cbor::decode_map_entries_slices_notrail(dec)?;
195        let mut remaining = cbor::MapEntries::new(entries);
196        let value = <Self as MapDecode>::decode_map(&mut remaining)?;
197        if !remaining.is_empty() {
198            let unknown: Vec<i64> = remaining.remaining_keys();
199            return Err(cbor::Error::DecodeFailed(format!(
200                "unknown CBOR map keys: {unknown:?}"
201            )));
202        }
203        Ok(value)
204    }
205}
206
207impl<T: ConfirmKey> MapEncode for Confirm<T> {
208    fn encode_map(&self, enc: &mut MapEncodeBuffer) -> Result<(), cbor::Error> {
209        let cnf = CnfMap {
210            cose_key: CoseKey {
211                kty: T::ALGORITHM,
212                x: self.key.to_confirm_bytes(),
213            },
214        };
215        enc.push(8, &cnf)
216    }
217}
218
219impl<T: ConfirmKey> MapDecode for Confirm<T> {
220    fn cbor_map_keys() -> &'static [i64] {
221        &[8]
222    }
223
224    fn decode_map<'a, E: MapEntryAccess<'a>>(entries: &mut E) -> Result<Self, cbor::Error> {
225        let raw = entries.take(8).ok_or(cbor::Error::DecodeFailed(
226            "missing required key 8 (cnf)".into(),
227        ))?;
228        let cnf = CnfMap::decode_cbor(raw)?;
229
230        // Verify the algorithm matches the expected key type
231        if cnf.cose_key.kty != T::ALGORITHM {
232            return Err(cbor::Error::DecodeFailed(format!(
233                "cnf: unexpected key type {}, want {}",
234                cnf.cose_key.kty,
235                T::ALGORITHM,
236            )));
237        }
238        let key = T::from_confirm_bytes(&cnf.cose_key.x)?;
239        Ok(Self { key })
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246
247    /// Verifies round-trip encoding of a Confirm<xdsa::PublicKey>.
248    #[test]
249    fn test_confirm_xdsa() {
250        let key = xdsa::SecretKey::generate().public_key();
251
252        #[derive(Debug, Cbor)]
253        struct Token {
254            #[cbor(embed)]
255            cnf: Confirm<xdsa::PublicKey>,
256        }
257        let orig = Token {
258            cnf: Confirm::new(key.clone()),
259        };
260        let data = cbor::encode(&orig).expect("encode");
261        let got = Token::decode_cbor(&data).expect("decode");
262
263        assert_eq!(got.cnf.key().to_bytes(), key.to_bytes());
264    }
265
266    /// Verifies round-trip encoding of a Confirm<xhpke::PublicKey>.
267    #[test]
268    fn test_confirm_xhpke() {
269        let key = xhpke::SecretKey::generate().public_key();
270
271        #[derive(Debug, Cbor)]
272        struct Token {
273            #[cbor(embed)]
274            cnf: Confirm<xhpke::PublicKey>,
275        }
276        let orig = Token {
277            cnf: Confirm::new(key.clone()),
278        };
279        let data = cbor::encode(&orig).expect("encode");
280        let got = Token::decode_cbor(&data).expect("decode");
281
282        assert_eq!(got.cnf.key().to_bytes(), key.to_bytes());
283    }
284
285    /// Verifies a struct embedding multiple claim types.
286    #[test]
287    fn test_composite_claims() {
288        let key = xdsa::SecretKey::generate().public_key();
289
290        #[derive(Debug, Cbor)]
291        struct DeviceCert {
292            #[cbor(embed)]
293            sub: Subject,
294            #[cbor(embed)]
295            exp: Expiration,
296            #[cbor(embed)]
297            nbf: NotBefore,
298            #[cbor(embed)]
299            cnf: Confirm<xdsa::PublicKey>,
300            #[cbor(key = 256)]
301            ueid: Vec<u8>,
302        }
303        let orig = DeviceCert {
304            sub: Subject {
305                sub: "device-abc-123".into(),
306            },
307            exp: Expiration { exp: 1000000 },
308            nbf: NotBefore { nbf: 100 },
309            cnf: Confirm::new(key.clone()),
310            ueid: b"SN-12345".to_vec(),
311        };
312        let data = cbor::encode(&orig).expect("encode");
313        let got = DeviceCert::decode_cbor(&data).expect("decode");
314
315        assert_eq!(got.sub.sub, "device-abc-123");
316        assert_eq!(got.exp.exp, 1000000);
317        assert_eq!(got.nbf.nbf, 100);
318        assert_eq!(got.cnf.key().to_bytes(), key.to_bytes());
319        assert_eq!(got.ueid, b"SN-12345");
320    }
321
322    /// Tests that decoding a cnf with mismatched kty fails.
323    #[test]
324    fn test_confirm_wrong_key_type() {
325        let key = xdsa::SecretKey::generate().public_key();
326
327        #[derive(Debug, Cbor)]
328        struct XdsaToken {
329            #[cbor(embed)]
330            cnf: Confirm<xdsa::PublicKey>,
331        }
332        #[derive(Debug, Cbor)]
333        struct XhpkeToken {
334            #[cbor(embed)]
335            cnf: Confirm<xhpke::PublicKey>,
336        }
337        let orig = XdsaToken {
338            cnf: Confirm::new(key),
339        };
340        let data = cbor::encode(&orig).expect("encode");
341
342        // Decoding as xhpke should fail due to key type mismatch
343        let err = XhpkeToken::decode_cbor(&data).expect_err("should fail with wrong key type");
344        let msg = err.to_string();
345        assert!(
346            msg.contains("unexpected key type"),
347            "expected key type error, got: {msg}"
348        );
349    }
350}