mod common;
use cbor2::Cbor;
use common::*;
use cose2::{
cwt::{Claims, ClaimsMap, Validator, ValidatorOptions},
iana, tag, KdfContext, PartyInfo, Sign1Message, SuppPubInfo,
};
#[test]
fn party_info_round_trip() {
let info = PartyInfo {
identity: Some(b"id".to_vec()),
nonce: Some(b"nonce".to_vec()),
other: None,
};
let bytes = cbor2::to_canonical_vec(&info).unwrap();
assert_eq!(bytes[0], 0x83);
let back: PartyInfo = cbor2::from_slice(&bytes).unwrap();
assert_eq!(back, info);
}
#[test]
fn supp_pub_info_two_and_three_elements() {
let mut info = SuppPubInfo {
key_data_length: 128,
..Default::default()
};
info.protected
.insert(iana::HeaderParameterAlg, iana::AlgorithmDirect_HKDF_SHA_256);
let bytes = cbor2::to_canonical_vec(&info).unwrap();
assert_eq!(bytes[0], 0x82); let back: SuppPubInfo = cbor2::from_slice(&bytes).unwrap();
assert_eq!(back, info);
info.other = Some(b"extra".to_vec());
let bytes = cbor2::to_canonical_vec(&info).unwrap();
assert_eq!(bytes[0], 0x83); let back: SuppPubInfo = cbor2::from_slice(&bytes).unwrap();
assert_eq!(back, info);
}
#[test]
fn kdf_context_four_and_five_elements() {
let mut ctx = KdfContext {
algorithm_id: iana::AlgorithmA128GCM,
party_u_info: PartyInfo {
identity: Some(b"u".to_vec()),
..Default::default()
},
party_v_info: PartyInfo {
nonce: Some(b"v".to_vec()),
..Default::default()
},
supp_pub_info: SuppPubInfo {
key_data_length: 128,
..Default::default()
},
supp_priv_info: None,
};
let bytes = ctx.to_vec().unwrap();
assert_eq!(bytes[0], 0x84); let back = KdfContext::from_slice(&bytes).unwrap();
assert_eq!(back, ctx);
ctx.supp_priv_info = Some(b"private".to_vec());
let bytes = ctx.to_vec().unwrap();
assert_eq!(bytes[0], 0x85); let back = KdfContext::from_slice(&bytes).unwrap();
assert_eq!(back, ctx);
}
#[test]
fn kdf_context_decode_errors_on_truncated_arrays() {
assert!(KdfContext::from_slice(&[0x80]).is_err());
assert!(cbor2::from_slice::<SuppPubInfo>(&[0x81, 0x18, 0x80]).is_err());
assert!(cbor2::from_slice::<SuppPubInfo>(&[0x80]).is_err());
}
#[test]
fn claims_round_trip_integer_keys() {
let claims = Claims {
issuer: Some("ldc:ca".into()),
subject: Some("ldc:chain".into()),
audience: Some("ldc:txpool".into()),
expiration: Some(1_700_000_300),
not_before: Some(1_700_000_000),
issued_at: Some(1_700_000_000),
cwt_id: Some(vec![0xa, 0xb, 0xc]),
..Default::default()
};
let bytes = claims.to_vec().unwrap();
assert_eq!(Claims::TAG, Some(iana::CBORTagCWT));
assert_eq!(&bytes[..2], tag::CWT_PREFIX);
assert_eq!(bytes[2], 0xa7);
let back = Claims::from_slice(&bytes).unwrap();
assert_eq!(back, claims);
let untagged = tag::skip_tag(tag::CWT_PREFIX, &bytes);
assert_eq!(cbor2::from_slice::<Claims>(untagged).unwrap(), claims);
assert_eq!(Claims::from_slice(untagged).unwrap(), claims);
let wrong_tagged = tag::with_tag(tag::SIGN1_PREFIX, untagged);
assert!(Claims::from_slice(&wrong_tagged).is_err());
}
#[test]
fn claims_omit_absent_fields_and_json() {
let claims = Claims {
issuer: Some("iss".into()),
..Default::default()
};
let bytes = claims.to_vec().unwrap();
assert_eq!(&bytes[..2], tag::CWT_PREFIX);
assert_eq!(bytes[2], 0xa1);
let empty = Claims::new();
assert_eq!(empty, Claims::default());
assert_eq!(empty.to_vec().unwrap(), vec![0xd8, 0x3d, 0xa0]);
}
#[test]
fn claims_preserve_extra_claims() {
let mut map = ClaimsMap::new();
map.insert(iana::CWTClaimIss, "iss");
map.insert(99, "unknown");
map.insert("private", true);
let bytes = map.to_vec().unwrap();
let claims = Claims::from_slice(&bytes).unwrap();
assert_eq!(claims.issuer.as_deref(), Some("iss"));
assert_eq!(claims.extra.get_text(99).unwrap(), Some("unknown"));
assert_eq!(claims.extra.get_bool("private").unwrap(), Some(true));
let direct = cbor2::from_slice::<Claims>(&bytes).unwrap();
assert_eq!(direct, claims);
let encoded = claims.to_vec().unwrap();
assert_eq!(&encoded[..2], tag::CWT_PREFIX);
assert_eq!(encoded[2], 0xa3);
let round_trip = Claims::from_slice(&encoded).unwrap();
assert_eq!(round_trip, claims);
let canonical = cbor2::to_canonical_vec(&claims).unwrap();
assert_eq!(canonical, encoded);
assert_eq!(cbor2::from_slice::<Claims>(&canonical).unwrap(), claims);
}
#[test]
fn cwt_in_sign1_round_trip() {
let signer = MockSigner::new(iana::AlgorithmEdDSA, b"ca");
let verifier = MockVerifier::new(iana::AlgorithmEdDSA, b"ca");
let claims = Claims {
issuer: Some("ldc:ca".into()),
audience: Some("ldc:txpool".into()),
expiration: Some(4_000_000_000),
..Default::default()
};
let mut msg = Sign1Message::new(Some(claims.to_vec().unwrap()));
let encoded = msg.sign_and_encode(&signer, None).unwrap();
let verified = Sign1Message::verify_and_decode(&verifier, &encoded, None).unwrap();
let decoded = Claims::from_slice(verified.payload.as_deref().unwrap()).unwrap();
assert_eq!(decoded, claims);
}
fn validator(opts: ValidatorOptions) -> Validator {
Validator::new(opts).unwrap()
}
#[test]
fn validator_rejects_excessive_clock_skew() {
let err = Validator::new(ValidatorOptions {
clock_skew_secs: 60 * 60,
..Default::default()
});
assert!(err.is_err());
}
#[test]
fn validator_validates_typed_claims() {
let v = validator(ValidatorOptions {
expected_issuer: Some("iss".into()),
expected_audience: Some("aud".into()),
clock_skew_secs: 60,
fixed_now: Some(1_000),
..Default::default()
});
let claims = Claims {
issuer: Some("iss".into()),
audience: Some("aud".into()),
expiration: Some(2_000),
not_before: Some(500),
issued_at: Some(500),
..Default::default()
};
assert!(v.validate(&claims).is_ok());
}
#[test]
fn validator_time_failures() {
let base = ValidatorOptions {
clock_skew_secs: 0,
fixed_now: Some(1_000),
..Default::default()
};
assert!(validator(base.clone())
.validate(&Claims::default())
.is_err());
let allow = validator(ValidatorOptions {
allow_missing_expiration: true,
..base.clone()
});
assert!(allow.validate(&Claims::default()).is_ok());
let expired = Claims {
expiration: Some(500),
..Default::default()
};
assert!(validator(base.clone()).validate(&expired).is_err());
let future = Claims {
expiration: Some(2_000),
not_before: Some(1_500),
..Default::default()
};
assert!(validator(base.clone()).validate(&future).is_err());
let iat_future = Claims {
expiration: Some(2_000),
issued_at: Some(1_500),
..Default::default()
};
let check_iat = validator(ValidatorOptions {
expect_issued_in_the_past: true,
..base.clone()
});
assert!(check_iat.validate(&iat_future).is_err());
assert!(validator(base.clone()).validate(&iat_future).is_ok());
}
#[test]
fn validator_identity_failures() {
let base = ValidatorOptions {
allow_missing_expiration: true,
fixed_now: Some(1_000),
..Default::default()
};
let iss = validator(ValidatorOptions {
expected_issuer: Some("right".into()),
..base.clone()
});
assert!(iss
.validate(&Claims {
issuer: Some("wrong".into()),
..Default::default()
})
.is_err());
let aud = validator(ValidatorOptions {
expected_audience: Some("right".into()),
..base.clone()
});
assert!(aud
.validate(&Claims {
issuer: Some("x".into()),
audience: Some("wrong".into()),
..Default::default()
})
.is_err());
}
#[test]
fn validator_validates_claims_map() {
let v = validator(ValidatorOptions {
expected_issuer: Some("iss".into()),
expected_audience: Some("aud".into()),
expect_issued_in_the_past: true,
clock_skew_secs: 60,
fixed_now: Some(1_000),
..Default::default()
});
let mut map = ClaimsMap::new();
map.insert(iana::CWTClaimIss, "iss");
map.insert(iana::CWTClaimAud, "aud");
map.insert(iana::CWTClaimExp, 2_000i64);
map.insert(iana::CWTClaimNbf, 500i64);
map.insert(iana::CWTClaimIat, 500i64);
assert!(v.validate_map(&map).is_ok());
let mut expired = ClaimsMap::new();
expired.insert(iana::CWTClaimExp, 100i64);
assert!(v.validate_map(&expired).is_err());
let mut bad = ClaimsMap::new();
bad.insert(iana::CWTClaimExp, "soon");
assert!(v.validate_map(&bad).is_err());
}
#[test]
fn validator_uses_system_clock_when_now_unset() {
let v = validator(ValidatorOptions {
clock_skew_secs: 0,
..Default::default()
});
let claims = Claims {
expiration: Some(u64::MAX), not_before: Some(0),
..Default::default()
};
assert!(v.validate(&claims).is_ok());
}