use mtcs::{
CaTrust, CosignerGroup, CosignerInfo, Error, SignatureAlgorithm, TrustAnchorId, TrustStore,
TrustedSubtree, VerificationKind, verify_certificate, verify_certificate_ignoring_validity,
};
mod keys;
const STANDALONE: &[u8] = include_bytes!("fixtures/standalone.der");
const STANDALONE_CA_ONLY: &[u8] = include_bytes!("fixtures/standalone_ca_only.der");
const UNUSED_BIT_BAD: &[u8] = include_bytes!("fixtures/unused_bit_bad.der");
const BITFLIP_BAD: &[u8] = include_bytes!("fixtures/bitflip_bad.der");
const LANDMARK_32: &[u8] = include_bytes!("fixtures/landmark_32.der");
const LANDMARK_2121: &[u8] = include_bytes!("fixtures/landmark_2121.der");
const CA: &[u8] = include_bytes!("fixtures/ca.der");
const NOT_BEFORE: i64 = 1577836800; const NOT_AFTER: i64 = 1924991999; const NOW: i64 = 1735689600;
fn id(dotted: &str) -> TrustAnchorId {
TrustAnchorId::from_dotted(dotted).unwrap()
}
fn cosigner(dotted: &str, algorithm: SignatureAlgorithm, spki_hex: &str) -> CosignerInfo {
CosignerInfo {
id: id(dotted),
algorithm,
spki_der: hex::decode(spki_hex).unwrap(),
}
}
fn store() -> TrustStore {
let mut store = TrustStore::new();
store.add_ca(CaTrust::from_ca_certificate(CA).unwrap());
store
}
fn hash(hex_str: &str) -> [u8; 32] {
hex::decode(hex_str).unwrap().try_into().unwrap()
}
#[test]
fn standalone_certificate_verifies_with_ca_cosignature_only() {
let store = store();
let verification = verify_certificate(&store, STANDALONE, NOW).unwrap();
assert_eq!(verification.kind, VerificationKind::Standalone);
assert_eq!(verification.ca_id, id("32473.1"));
assert_eq!((verification.log_number, verification.entry_index), (1, 10));
assert!(verification.satisfied_cosigners.contains(&id("32473.1")));
assert_eq!(verification.satisfied_cosigners.len(), 1);
let verification = verify_certificate(&store, STANDALONE_CA_ONLY, NOW).unwrap();
assert_eq!(verification.kind, VerificationKind::Standalone);
assert_eq!(verification.satisfied_cosigners, [id("32473.1")]);
}
#[test]
fn additional_required_cosigners_are_enforced() {
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.require_cosigner(cosigner(
"32473.2.1",
SignatureAlgorithm::EcdsaP256Sha256,
keys::C_32473_2_1,
))
.unwrap();
ca.require_cosigner(cosigner(
"32473.2.2",
SignatureAlgorithm::MlDsa65,
keys::C_32473_2_2,
))
.unwrap();
let mut with_extras = TrustStore::new();
with_extras.add_ca(ca);
let verification = verify_certificate(&with_extras, STANDALONE, NOW).unwrap();
assert_eq!(verification.satisfied_cosigners.len(), 3);
assert!(matches!(
verify_certificate(&with_extras, STANDALONE_CA_ONLY, NOW),
Err(Error::PolicyUnsatisfied { .. })
));
let mut strict = store();
strict.required_for_all.push(CosignerGroup::single(cosigner(
"32473.2.1",
SignatureAlgorithm::EcdsaP256Sha256,
keys::C_32473_2_1,
)));
assert!(verify_certificate(&strict, STANDALONE, NOW).is_ok());
assert!(matches!(
verify_certificate(&strict, STANDALONE_CA_ONLY, NOW),
Err(Error::PolicyUnsatisfied { .. })
));
}
#[test]
fn tampered_and_misencoded_certificates_fail() {
let store = store();
assert_eq!(
verify_certificate(&store, BITFLIP_BAD, NOW),
Err(Error::InvalidCosignature(id("32473.1")))
);
assert!(matches!(
verify_certificate(&store, UNUSED_BIT_BAD, NOW),
Err(Error::Malformed(_))
));
}
#[test]
fn revocation_validity_and_ca_membership_are_checked() {
let store = store();
let mut revoked = store.clone();
revoked.cas[0].revoke_range(1 << 48 | 10, 1 << 48 | 10);
assert_eq!(
verify_certificate(&revoked, STANDALONE, NOW),
Err(Error::Revoked)
);
let mut cert = STANDALONE.to_vec();
let pos = cert.windows(2).position(|w| w == [0x02, 0x07]).unwrap();
assert_eq!(cert[pos + 2], 1);
cert[pos + 2] = 6;
assert_eq!(verify_certificate(&store, &cert, NOW), Err(Error::Revoked));
assert_eq!(
verify_certificate(&store, STANDALONE, NOT_BEFORE - 1),
Err(Error::TimeInvalid)
);
assert_eq!(
verify_certificate(&store, STANDALONE, NOT_AFTER + 1),
Err(Error::TimeInvalid)
);
assert!(verify_certificate_ignoring_validity(&store, STANDALONE).is_ok());
assert_eq!(
verify_certificate(&TrustStore::new(), STANDALONE, NOW),
Err(Error::UnknownCa)
);
}
#[test]
fn non_mtc_certificates_are_rejected() {
let der = pem_der(include_bytes!("fixtures/plain.pem"));
assert_eq!(verify_certificate(&store(), &der, NOW), Err(Error::NotMtc));
}
fn pem_der(pem: &[u8]) -> Vec<u8> {
let text = core::str::from_utf8(pem).unwrap();
let start = text.find("-----BEGIN CERTIFICATE-----").unwrap() + 27;
let end = text.find("-----END CERTIFICATE-----").unwrap();
let body: String = text[start..end]
.chars()
.filter(|c| !c.is_whitespace())
.collect();
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(body)
.unwrap()
}
#[test]
fn landmark_relative_certificates_verify_against_trusted_subtrees() {
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.trust_subtree(TrustedSubtree {
log_number: 1,
start: 32,
end: 34,
hash: hash(keys::SUBTREE_32_34),
});
ca.trust_subtree(TrustedSubtree {
log_number: 1,
start: 2048,
end: 2122,
hash: hash(keys::SUBTREE_2048_2122),
});
let mut with_subtrees = TrustStore::new();
with_subtrees.add_ca(ca);
let verification = verify_certificate(&with_subtrees, LANDMARK_32, NOW).unwrap();
assert_eq!(verification.kind, VerificationKind::LandmarkRelative);
assert_eq!((verification.log_number, verification.entry_index), (1, 32));
assert!(verification.satisfied_cosigners.is_empty());
let verification = verify_certificate(&with_subtrees, LANDMARK_2121, NOW).unwrap();
assert_eq!(verification.kind, VerificationKind::LandmarkRelative);
assert_eq!(
(verification.log_number, verification.entry_index),
(1, 2121)
);
let mut wrong = with_subtrees.clone();
wrong.cas[0].trusted_subtrees[0].hash[0] ^= 1;
assert_eq!(
verify_certificate(&wrong, LANDMARK_32, NOW),
Err(Error::ProofEvaluation)
);
assert!(matches!(
verify_certificate(&store(), LANDMARK_32, NOW),
Err(Error::PolicyUnsatisfied { .. })
));
}
#[test]
fn ca_certificate_parses_into_trust_configuration() {
let ca = CaTrust::from_ca_certificate(CA).unwrap();
assert_eq!(ca.ca_id, id("32473.1"));
assert_eq!(ca.ca_cosigner.algorithm, SignatureAlgorithm::MlDsa44);
assert_eq!(ca.ca_cosigner.id, ca.ca_id);
assert_eq!(
ca.ca_cosigner.spki_der,
hex::decode(keys::C_32473_1).unwrap()
);
assert_eq!(ca.revoked_ranges.len(), 2);
assert_eq!(ca.revoked_ranges[0], 0..=(1 << 48) - 1);
assert_eq!(
ca.revoked_ranges[1],
((5 << 48) | 0xffff_ffff_ffff) + 1..=u64::MAX
);
}
#[test]
fn cosigner_groups_support_k_of_n_quorums() {
let p256 = cosigner(
"32473.2.1",
SignatureAlgorithm::EcdsaP256Sha256,
keys::C_32473_2_1,
);
let mldsa65 = cosigner("32473.2.2", SignatureAlgorithm::MlDsa65, keys::C_32473_2_2);
let absent = cosigner("32473.2.3", SignatureAlgorithm::MlDsa87, keys::C_32473_2_3);
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.require_group(CosignerGroup::new(vec![p256.clone(), mldsa65.clone()], 1).unwrap())
.unwrap();
ca.require_group(CosignerGroup::new(vec![p256.clone(), mldsa65.clone()], 2).unwrap())
.unwrap();
let mut store = TrustStore::new();
store.add_ca(ca);
let verification = verify_certificate(&store, STANDALONE, NOW).unwrap();
assert_eq!(verification.satisfied_cosigners.len(), 3);
let Err(Error::PolicyUnsatisfied { missing, invalid }) =
verify_certificate(&store, STANDALONE_CA_ONLY, NOW)
else {
panic!("expected PolicyUnsatisfied")
};
assert!(missing.contains(&id("32473.2.1")) && missing.contains(&id("32473.2.2")));
assert!(invalid.is_empty());
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.require_group(CosignerGroup::new(vec![absent.clone()], 1).unwrap())
.unwrap();
let mut store = TrustStore::new();
store.add_ca(ca);
let Err(Error::PolicyUnsatisfied { missing, .. }) = verify_certificate(&store, STANDALONE, NOW)
else {
panic!("expected PolicyUnsatisfied")
};
assert_eq!(missing, [id("32473.2.3")]);
}
#[test]
fn cosigner_groups_validate_their_threshold() {
let member = cosigner(
"32473.2.1",
SignatureAlgorithm::EcdsaP256Sha256,
keys::C_32473_2_1,
);
assert!(matches!(
CosignerGroup::new(vec![member.clone()], 0),
Err(Error::Configuration(_))
));
assert!(matches!(
CosignerGroup::new(vec![member.clone()], 2),
Err(Error::Configuration(_))
));
assert!(matches!(
CosignerGroup::new(vec![], 1),
Err(Error::Configuration(_))
));
assert!(CosignerGroup::new(vec![member], 1).is_ok());
}
#[test]
fn trust_store_and_cosignature_failures_are_distinguished() {
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.require_cosigner(CosignerInfo {
id: id("32473.2.1"),
algorithm: SignatureAlgorithm::EcdsaP256Sha256,
spki_der: vec![0x30, 0x03, 0x06, 0x01, 0x00], })
.unwrap();
let mut store = TrustStore::new();
store.add_ca(ca);
assert!(matches!(
verify_certificate(&store, STANDALONE, NOW),
Err(Error::UnsupportedKey(_))
));
let mut wrong_spki = hex::decode(keys::C_32473_2_2).unwrap();
let last = wrong_spki.len() - 1;
wrong_spki[last] ^= 1; let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.require_cosigner(CosignerInfo {
id: id("32473.2.2"),
algorithm: SignatureAlgorithm::MlDsa65,
spki_der: wrong_spki,
})
.unwrap();
let mut store = TrustStore::new();
store.add_ca(ca);
let Err(Error::PolicyUnsatisfied { missing, invalid }) =
verify_certificate(&store, STANDALONE, NOW)
else {
panic!("expected PolicyUnsatisfied")
};
assert_eq!(invalid, [id("32473.2.2")]);
assert!(!missing.contains(&id("32473.2.2")));
}
#[test]
fn cosigner_lookup_resolves_per_ca_groups_before_global_groups() {
let clean = cosigner(
"32473.2.1",
SignatureAlgorithm::EcdsaP256Sha256,
keys::C_32473_2_1,
);
let mut corrupted = clean.clone();
let last = corrupted.spki_der.len() - 1;
corrupted.spki_der[last] ^= 1;
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.require_group(CosignerGroup::single(clean.clone()))
.unwrap();
let mut store = TrustStore::new();
store.add_ca(ca);
store.require_group_for_all(CosignerGroup::single(corrupted.clone()));
let verification = verify_certificate(&store, STANDALONE, NOW).unwrap();
assert!(verification.satisfied_cosigners.contains(&id("32473.2.1")));
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.require_group(CosignerGroup::single(corrupted.clone()))
.unwrap();
let mut store = TrustStore::new();
store.add_ca(ca);
store.require_group_for_all(CosignerGroup::single(clean));
let Err(Error::PolicyUnsatisfied { invalid, .. }) = verify_certificate(&store, STANDALONE, NOW)
else {
panic!("expected PolicyUnsatisfied")
};
assert_eq!(invalid, [id("32473.2.1")]);
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.require_group(CosignerGroup::single(corrupted)).unwrap();
ca.require_group(CosignerGroup::single(cosigner(
"32473.2.1",
SignatureAlgorithm::EcdsaP256Sha256,
keys::C_32473_2_1,
)))
.unwrap();
let mut store = TrustStore::new();
store.add_ca(ca);
let Err(Error::PolicyUnsatisfied { invalid, .. }) = verify_certificate(&store, STANDALONE, NOW)
else {
panic!("expected PolicyUnsatisfied")
};
assert_eq!(invalid, [id("32473.2.1")]);
}
#[test]
fn verification_exposes_the_subtree_and_its_hash() {
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.trust_subtree(TrustedSubtree {
log_number: 1,
start: 32,
end: 34,
hash: hash(keys::SUBTREE_32_34),
});
let mut store = TrustStore::new();
store.add_ca(ca);
let verification = verify_certificate(&store, LANDMARK_32, NOW).unwrap();
assert_eq!(verification.subtree, mtcs::Subtree { start: 32, end: 34 });
assert_eq!(verification.subtree_hash, hash(keys::SUBTREE_32_34));
}
#[test]
fn ca_certificate_profile_is_enforced() {
let mut not_critical = CA.to_vec();
let needle = hex::decode("06082b0601050507012601 01ff".replace(' ', "")).unwrap();
let pos = not_critical
.windows(needle.len())
.position(|w| w == needle.as_slice())
.unwrap();
let last = pos + needle.len() - 1;
not_critical[last] = 0x00; assert!(CaTrust::from_ca_certificate(¬_critical).is_err());
let mut not_ca = CA.to_vec();
let needle = hex::decode("0603551d13").unwrap();
let pos = not_ca
.windows(needle.len())
.position(|w| w == needle.as_slice())
.unwrap();
let window_end = pos + 20;
let inner = not_ca[pos..window_end]
.windows(3)
.rposition(|w| w == [0x01, 0x01, 0xff])
.unwrap();
not_ca[pos + inner + 2] = 0x00; assert!(CaTrust::from_ca_certificate(¬_ca).is_err());
let mut no_cert_sign = CA.to_vec();
let needle = hex::decode("0603551d0f").unwrap();
let pos = no_cert_sign
.windows(needle.len())
.position(|w| w == needle.as_slice())
.unwrap();
let window_end = pos + 20;
let inner = no_cert_sign[pos..window_end]
.windows(4)
.position(|w| w == [0x03, 0x02, 0x02, 0x04])
.unwrap();
no_cert_sign[pos + inner + 3] = 0x40; assert!(CaTrust::from_ca_certificate(&no_cert_sign).is_err());
}
#[test]
fn ca_cosigner_cannot_join_transparency_groups() {
let ca_key = CosignerInfo {
id: id("32473.1"),
algorithm: SignatureAlgorithm::MlDsa44,
spki_der: hex::decode(keys::C_32473_1).unwrap(),
};
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
assert!(ca.require_cosigner(ca_key.clone()).is_err());
assert!(
ca.require_group(CosignerGroup::new(vec![ca_key.clone()], 1).unwrap())
.is_err()
);
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.required_groups.push(
CosignerGroup::new(
vec![
ca_key.clone(),
cosigner(
"32473.2.1",
SignatureAlgorithm::EcdsaP256Sha256,
keys::C_32473_2_1,
),
],
2,
)
.unwrap(),
);
let mut store = TrustStore::new();
store.add_ca(ca);
assert!(matches!(
verify_certificate(&store, STANDALONE, NOW),
Err(Error::PolicyUnsatisfied { .. })
));
}
#[test]
fn missing_lists_candidates_not_all_required_cosigners() {
let mldsa65 = cosigner("32473.2.2", SignatureAlgorithm::MlDsa65, keys::C_32473_2_2);
let absent1 = cosigner("32473.2.3", SignatureAlgorithm::MlDsa87, keys::C_32473_2_3);
let absent2 = cosigner("32473.3.1", SignatureAlgorithm::MlDsa44, keys::C_32473_3_1);
let mut ca = CaTrust::from_ca_certificate(CA).unwrap();
ca.require_group(
CosignerGroup::new(vec![mldsa65, absent1.clone(), absent2.clone()], 2).unwrap(),
)
.unwrap();
let mut store = TrustStore::new();
store.add_ca(ca);
let Err(Error::PolicyUnsatisfied { missing, .. }) = verify_certificate(&store, STANDALONE, NOW)
else {
panic!("expected PolicyUnsatisfied")
};
assert_eq!(missing, [id("32473.2.3"), id("32473.3.1")]);
}
#[test]
fn truncated_and_tampered_certificates_never_panic() {
let store = store();
let fixtures: [&[u8]; 7] = [
STANDALONE,
STANDALONE_CA_ONLY,
UNUSED_BIT_BAD,
BITFLIP_BAD,
LANDMARK_32,
LANDMARK_2121,
CA,
];
for fixture in fixtures {
for end in 0..fixture.len() {
assert!(
verify_certificate(&store, &fixture[..end], NOW).is_err(),
"prefix of {end} bytes unexpectedly verified"
);
}
for position in (0..fixture.len()).step_by(17) {
let mut tampered = fixture.to_vec();
tampered[position] ^= 1;
let _ = verify_certificate(&store, &tampered, NOW);
}
}
}
#[test]
fn trust_store_routes_between_multiple_cas() {
let fabricated = TrustAnchorId::from_dotted("32473.99").unwrap();
let mut other = CaTrust::new(
fabricated.clone(),
CosignerInfo {
id: fabricated,
algorithm: SignatureAlgorithm::MlDsa44,
spki_der: hex::decode(keys::C_32473_1).unwrap(),
},
);
other.revoke_range(0, u64::MAX); let mut multi = store();
multi.add_ca(other);
assert!(verify_certificate(&multi, STANDALONE, NOW).is_ok());
let mut strict = store();
strict.require_group_for_all(CosignerGroup::single(cosigner(
"32473.2.1",
SignatureAlgorithm::EcdsaP256Sha256,
keys::C_32473_2_1,
)));
assert!(verify_certificate(&strict, STANDALONE, NOW).is_ok());
assert!(matches!(
verify_certificate(&strict, STANDALONE_CA_ONLY, NOW),
Err(Error::PolicyUnsatisfied { .. })
));
}