darkbio-crypto 0.15.0

Cryptography primitives and wrappers
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
// crypto-rs: cryptography primitives and wrappers
// Copyright 2026 Dark Bio AG. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//! CBOR Web Tokens on top of COSE Sign1.
//!
//! <https://datatracker.ietf.org/doc/html/rfc8392>
//!
//! Tokens carry a set of claims encoded as a CBOR map. Standard claims are
//! provided as embeddable single-field structs ([`claims::Issuer`],
//! [`claims::Subject`], etc.) that can be composed into application-specific
//! token types.
//!
//! # Example
//!
//! ```ignore
//! use darkbio_crypto::cbor::Cbor;
//! use darkbio_crypto::cwt::{self, claims};
//! use darkbio_crypto::xdsa;
//!
//! #[derive(Cbor)]
//! struct DeviceCert {
//!     #[cbor(embed)]
//!     sub: claims::Subject,
//!     #[cbor(embed)]
//!     exp: claims::Expiration,
//!     #[cbor(embed)]
//!     nbf: claims::NotBefore,
//!     #[cbor(embed)]
//!     cnf: claims::Confirm<xdsa::PublicKey>,
//!     #[cbor(key = 256)]
//!     ueid: Vec<u8>,
//! }
//!
//! let token = cwt::issue(&cert, &signer_key, b"device-cert").unwrap();
//! let verified: DeviceCert = cwt::verify(&token, &issuer_pub, b"device-cert", Some(now)).unwrap();
//! ```

pub mod claims;

use crate::cbor::{self, Decode, Encode, Raw};
use crate::{cose, xdsa};

/// Error is the failures that can occur during CWT operations.
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum Error {
    #[error("cbor: {0}")]
    Cbor(#[from] cbor::Error),
    #[error("cose: {0}")]
    Cose(#[from] cose::Error),
    #[error("missing nbf claim")]
    MissingNbf,
    #[error("duplicate claim key {0}")]
    DuplicateKey(i64),
    #[error("token not yet valid: nbf {nbf} > now {now}")]
    NotYetValid { nbf: u64, now: u64 },
    #[error("token already expired: exp {exp} <= now {now}")]
    AlreadyExpired { exp: u64, now: u64 },
}

/// issue signs a set of claims as a CWT using COSE Sign1.
///
/// The claims value must be a struct whose fields encode as a CBOR map
/// (using `#[cbor(key = N)]` tags and/or embedded claim types).
///
/// Uses the current system time as the COSE signature timestamp.
pub fn issue(
    claims: &impl Encode,
    signer: &xdsa::SecretKey,
    domain: &[u8],
) -> Result<Vec<u8>, Error> {
    let claims_bytes = cbor::encode(claims)?;
    Ok(cose::sign(Raw(claims_bytes), cbor::NULL, signer, domain)?)
}

/// issue_at signs a set of claims as a CWT with an explicit COSE timestamp.
///
/// This is primarily useful for testing with deterministic timestamps.
pub fn issue_at(
    claims: &impl Encode,
    signer: &xdsa::SecretKey,
    domain: &[u8],
    timestamp: i64,
) -> Result<Vec<u8>, Error> {
    let claims_bytes = cbor::encode(claims)?;
    Ok(cose::sign_at(
        Raw(claims_bytes),
        cbor::NULL,
        signer,
        domain,
        timestamp,
    )?)
}

/// verify verifies a CWT's COSE signature and temporal validity, then decodes
/// the claims into T.
///
/// When `now` is `Some`, temporal claims are validated: nbf (key 5) must be
/// present and `nbf <= now`, and if exp (key 4) is present then `now < exp`.
/// When `now` is `None`, temporal validation is skipped entirely.
pub fn verify<T: Decode>(
    data: &[u8],
    verifier: &xdsa::PublicKey,
    domain: &[u8],
    now: Option<u64>,
) -> Result<T, Error> {
    // Verify COSE signature (skip COSE drift check — CWT handles temporal validation)
    let raw: Raw = cose::verify(data, cbor::NULL, verifier, domain, None)?;

    // Extract and validate temporal claims if requested
    if let Some(now) = now {
        let (nbf, exp) = read_temporal_claims(&raw)?;
        if now < nbf {
            return Err(Error::NotYetValid { nbf, now });
        }
        if let Some(exp) = exp
            && now >= exp
        {
            return Err(Error::AlreadyExpired { exp, now });
        }
    }
    // Decode claims into T
    Ok(cbor::decode(&raw.0)?)
}

/// signer extracts the signer's fingerprint from a CWT without verifying
/// the signature. The returned data is unauthenticated.
pub fn signer(data: &[u8]) -> Result<xdsa::Fingerprint, Error> {
    Ok(cose::signer(data)?)
}

/// peek extracts and decodes claims from a CWT without verifying the signature.
///
/// **Warning**: This function does NOT verify the signature. The returned payload
/// is unauthenticated and should not be trusted until verified with [`verify`].
/// Use [`signer`] to extract the signer's fingerprint for key lookup. The single
/// case for this method is self-signed key discovery.
pub fn peek<T: Decode>(data: &[u8]) -> Result<T, Error> {
    let raw: Raw = cose::peek(data)?;
    Ok(cbor::decode(&raw.0)?)
}

/// Reads temporal claims (exp key 4, nbf key 5) from raw CBOR map bytes.
/// Returns nbf (required) and exp (optional, None if absent).
fn read_temporal_claims(raw: &[u8]) -> Result<(u64, Option<u64>), Error> {
    let mut dec = cbor::Decoder::new(raw);
    let n = dec.decode_map_header()?;

    let mut nbf: Option<u64> = None;
    let mut exp: Option<u64> = None;

    for _ in 0..n {
        let key = dec.decode_int()?;
        match key {
            4 => {
                // exp (reject duplicates)
                if exp.is_some() {
                    return Err(Error::DuplicateKey(4));
                }
                exp = Some(dec.decode_uint()?);
            }
            5 => {
                // nbf (reject duplicates)
                if nbf.is_some() {
                    return Err(Error::DuplicateKey(5));
                }
                nbf = Some(dec.decode_uint()?);
            }
            _ => {
                // Skip unknown claim value
                Raw::decode_cbor_notrail(&mut dec)?;
            }
        }
    }
    let nbf = nbf.ok_or(Error::MissingNbf)?;
    Ok((nbf, exp))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::cbor::Cbor;
    use crate::cwt::claims;
    use crate::cwt::claims::eat;

    /// simpleCert is the minimal token type used by most tests.
    #[derive(Debug, Cbor)]
    struct SimpleCert {
        #[cbor(embed)]
        sub: claims::Subject,
        #[cbor(embed)]
        exp: Option<claims::Expiration>,
        #[cbor(embed)]
        nbf: claims::NotBefore,
        #[cbor(embed)]
        cnf: claims::Confirm<xdsa::PublicKey>,
    }

    /// deviceCert is a composite token type with EAT claims.
    #[derive(Debug, Cbor)]
    struct DeviceCert {
        #[cbor(embed)]
        sub: claims::Subject,
        #[cbor(embed)]
        exp: Option<claims::Expiration>,
        #[cbor(embed)]
        nbf: claims::NotBefore,
        #[cbor(embed)]
        cnf: claims::Confirm<xdsa::PublicKey>,
        #[cbor(embed)]
        ueid: eat::Ueid,
    }

    /// Token type without NotBefore, used for missing-nbf test.
    #[derive(Debug, Cbor)]
    struct NoNbfCert {
        #[cbor(embed)]
        sub: claims::Subject,
        #[cbor(embed)]
        cnf: claims::Confirm<xdsa::PublicKey>,
    }

    /// Token type without Expiration, used for no-expiration test.
    #[derive(Debug, Cbor)]
    struct NoExpCert {
        #[cbor(embed)]
        sub: claims::Subject,
        #[cbor(embed)]
        nbf: claims::NotBefore,
        #[cbor(embed)]
        cnf: claims::Confirm<xdsa::PublicKey>,
    }

    /// Tests the happy path: issue a token and verify it.
    #[test]
    fn test_issue_verify() {
        let issuer = xdsa::SecretKey::generate();
        let device = xdsa::SecretKey::generate();

        let cert = DeviceCert {
            sub: claims::Subject {
                sub: "device-abc".into(),
            },
            exp: Some(claims::Expiration { exp: 2000000 }),
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(device.public_key()),
            ueid: eat::Ueid {
                ueid: b"SN-999".to_vec(),
            },
        };
        let token = issue(&cert, &issuer, b"test-domain").expect("issue");
        let got: DeviceCert =
            verify(&token, &issuer.public_key(), b"test-domain", Some(1500000)).expect("verify");

        assert_eq!(got.sub.sub, "device-abc");
        assert_eq!(got.exp.unwrap().exp, 2000000);
        assert_eq!(got.cnf.key().to_bytes(), device.public_key().to_bytes(),);
        assert_eq!(got.ueid.ueid, b"SN-999");
    }

    /// Tests that now=None skips temporal validation.
    #[test]
    fn test_verify_skip_time() {
        let issuer = xdsa::SecretKey::generate();

        let cert = SimpleCert {
            sub: claims::Subject { sub: "test".into() },
            exp: None,
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"test").expect("issue");
        let got: SimpleCert =
            verify(&token, &issuer.public_key(), b"test", None).expect("verify with None time");

        assert_eq!(got.sub.sub, "test");
    }

    /// Tests rejection when now < nbf.
    #[test]
    fn test_verify_not_yet_valid() {
        let issuer = xdsa::SecretKey::generate();

        let cert = SimpleCert {
            sub: claims::Subject { sub: "test".into() },
            exp: None,
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"test").expect("issue");
        let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(500000))
            .expect_err("should fail");

        assert!(matches!(err, Error::NotYetValid { .. }));
    }

    /// Tests rejection when now > exp.
    #[test]
    fn test_verify_expired() {
        let issuer = xdsa::SecretKey::generate();

        let cert = SimpleCert {
            sub: claims::Subject { sub: "test".into() },
            exp: Some(claims::Expiration { exp: 2000000 }),
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"test").expect("issue");
        let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(3000000))
            .expect_err("should fail");

        assert!(matches!(err, Error::AlreadyExpired { .. }));
    }

    /// Tests rejection when nbf is absent and time check is on.
    #[test]
    fn test_verify_missing_nbf() {
        let issuer = xdsa::SecretKey::generate();

        let cert = NoNbfCert {
            sub: claims::Subject { sub: "test".into() },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"test").expect("issue");
        let err = verify::<NoNbfCert>(&token, &issuer.public_key(), b"test", Some(1000000))
            .expect_err("should fail");

        assert!(matches!(err, Error::MissingNbf));
    }

    /// Tests rejection with wrong verifier key.
    #[test]
    fn test_verify_wrong_key() {
        let issuer = xdsa::SecretKey::generate();
        let wrong = xdsa::SecretKey::generate();

        let cert = SimpleCert {
            sub: claims::Subject { sub: "test".into() },
            exp: None,
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"test").expect("issue");

        assert!(verify::<SimpleCert>(&token, &wrong.public_key(), b"test", Some(1500000)).is_err());
    }

    /// Tests fingerprint extraction from a token.
    #[test]
    fn test_signer() {
        let issuer = xdsa::SecretKey::generate();

        let cert = SimpleCert {
            sub: claims::Subject { sub: "test".into() },
            exp: None,
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"test").expect("issue");
        let fp = signer(&token).expect("signer");

        assert_eq!(fp, issuer.public_key().fingerprint());
    }

    /// Tests unauthenticated claims extraction.
    #[test]
    fn test_peek() {
        let issuer = xdsa::SecretKey::generate();

        let cert = SimpleCert {
            sub: claims::Subject {
                sub: "peek-test".into(),
            },
            exp: None,
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"test").expect("issue");
        let got: SimpleCert = peek(&token).expect("peek");

        assert_eq!(got.sub.sub, "peek-test");
    }

    /// Tests rejection when the verification domain differs.
    #[test]
    fn test_verify_wrong_domain() {
        let issuer = xdsa::SecretKey::generate();

        let cert = SimpleCert {
            sub: claims::Subject { sub: "test".into() },
            exp: None,
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"domain-a").expect("issue");

        assert!(
            verify::<SimpleCert>(&token, &issuer.public_key(), b"domain-b", Some(1500000)).is_err()
        );
    }

    /// Tests that now == nbf passes and now == exp fails per RFC 8392.
    #[test]
    fn test_verify_boundary_exact() {
        let issuer = xdsa::SecretKey::generate();

        let cert = SimpleCert {
            sub: claims::Subject { sub: "test".into() },
            exp: Some(claims::Expiration { exp: 2000000 }),
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"test").expect("issue");

        // now == nbf should pass
        verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(1000000))
            .expect("now == nbf should pass");

        // now == exp should fail
        let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(2000000))
            .expect_err("now == exp should fail");
        assert!(matches!(err, Error::AlreadyExpired { .. }));
    }

    /// Tests that a token without exp passes time validation.
    #[test]
    fn test_verify_no_expiration() {
        let issuer = xdsa::SecretKey::generate();

        let cert = NoExpCert {
            sub: claims::Subject { sub: "test".into() },
            nbf: claims::NotBefore { nbf: 1000000 },
            cnf: claims::Confirm::new(xdsa::SecretKey::generate().public_key()),
        };
        let token = issue(&cert, &issuer, b"test").expect("issue");

        // Should pass even far in the future since there's no exp
        verify::<NoExpCert>(&token, &issuer.public_key(), b"test", Some(99999999))
            .expect("no exp should pass");
    }

    /// Tests that duplicate temporal claim keys are rejected.
    #[test]
    fn test_verify_duplicate_nbf() {
        let issuer = xdsa::SecretKey::generate();

        // Manually construct a CBOR map with duplicate nbf (key 5)
        let mut enc = cbor::Encoder::new();
        enc.encode_map_header(3);
        enc.encode_int(2);
        enc.encode_text("test");
        enc.encode_int(5);
        enc.encode_uint(1000000);
        enc.encode_int(5); // duplicate nbf
        enc.encode_uint(2000000);

        // Sign the raw payload via COSE (bypassing CWT's encoder)
        let token = cose::sign(Raw(enc.finish()), cbor::NULL, &issuer, b"test").expect("sign");

        // Verify should fail with DuplicateKey before claims decoding
        let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(1500000))
            .expect_err("should reject duplicate nbf");
        assert!(matches!(err, Error::DuplicateKey(5)));
    }
}