pub mod claims;
use crate::cbor::{self, Decode, Encode, Raw};
use crate::{cose, xdsa};
#[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 },
}
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)?)
}
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,
)?)
}
pub fn verify<T: Decode>(
data: &[u8],
verifier: &xdsa::PublicKey,
domain: &[u8],
now: Option<u64>,
) -> Result<T, Error> {
let raw: Raw = cose::verify(data, cbor::NULL, verifier, domain, None)?;
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 });
}
}
Ok(cbor::decode(&raw.0)?)
}
pub fn signer(data: &[u8]) -> Result<xdsa::Fingerprint, Error> {
Ok(cose::signer(data)?)
}
pub fn peek<T: Decode>(data: &[u8]) -> Result<T, Error> {
let raw: Raw = cose::peek(data)?;
Ok(cbor::decode(&raw.0)?)
}
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 => {
if exp.is_some() {
return Err(Error::DuplicateKey(4));
}
exp = Some(dec.decode_uint()?);
}
5 => {
if nbf.is_some() {
return Err(Error::DuplicateKey(5));
}
nbf = Some(dec.decode_uint()?);
}
_ => {
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;
#[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>,
}
#[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,
}
#[derive(Debug, Cbor)]
struct NoNbfCert {
#[cbor(embed)]
sub: claims::Subject,
#[cbor(embed)]
cnf: claims::Confirm<xdsa::PublicKey>,
}
#[derive(Debug, Cbor)]
struct NoExpCert {
#[cbor(embed)]
sub: claims::Subject,
#[cbor(embed)]
nbf: claims::NotBefore,
#[cbor(embed)]
cnf: claims::Confirm<xdsa::PublicKey>,
}
#[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");
}
#[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");
}
#[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 { .. }));
}
#[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 { .. }));
}
#[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));
}
#[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());
}
#[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());
}
#[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");
}
#[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()
);
}
#[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");
verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(1000000))
.expect("now == nbf should pass");
let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(2000000))
.expect_err("now == exp should fail");
assert!(matches!(err, Error::AlreadyExpired { .. }));
}
#[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");
verify::<NoExpCert>(&token, &issuer.public_key(), b"test", Some(99999999))
.expect("no exp should pass");
}
#[test]
fn test_verify_duplicate_nbf() {
let issuer = xdsa::SecretKey::generate();
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); enc.encode_uint(2000000);
let token = cose::sign(Raw(enc.finish()), cbor::NULL, &issuer, b"test").expect("sign");
let err = verify::<SimpleCert>(&token, &issuer.public_key(), b"test", Some(1500000))
.expect_err("should reject duplicate nbf");
assert!(matches!(err, Error::DuplicateKey(5)));
}
}