20260722 新增密码学签名验证缓存(#126):5个验签收口点接入独立缓存,observe-only/enable双开关默认关闭,消融脚本新增crypto-sig等case set,verification contract补crypto_signature字段
This commit is contained in:
@@ -485,9 +485,17 @@ fn verify_child_signature(
|
||||
child: &X509Certificate<'_>,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
) -> Result<(), CaPathError> {
|
||||
child
|
||||
.verify_signature(Some(issuer_spki))
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
||||
crate::crypto_sig_cache::verify_with_cache(
|
||||
crate::crypto_sig_cache::CryptoSigVerifyPoint::ChildCaCert,
|
||||
child.tbs_certificate.as_ref(),
|
||||
child.signature_value.data.as_ref(),
|
||||
issuer_spki.raw,
|
||||
|| {
|
||||
child
|
||||
.verify_signature(Some(issuer_spki))
|
||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_child_aki_matches_issuer_ski(
|
||||
@@ -2118,6 +2126,53 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_cache_stores_real_verify_success_and_skips_on_hit() {
|
||||
let _guard = crate::crypto_sig_cache::GLOBAL_TEST_LOCK.lock().expect("test lock");
|
||||
crate::crypto_sig_cache::clear_global();
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let (issuer, child, other) = gen_issuer_and_child_der(td.path());
|
||||
let issuer_cert = parse_x509_cert(&issuer).expect("x509 parse issuer");
|
||||
let child_cert = parse_x509_cert(&child).expect("x509 parse child");
|
||||
let other_cert = parse_x509_cert(&other).expect("x509 parse other");
|
||||
|
||||
let cache_dir = tempfile::tempdir().expect("tempdir");
|
||||
let cache = std::sync::Arc::new(crate::crypto_sig_cache::CryptoSigCache::load_or_rebuild(
|
||||
cache_dir.path().join("work-db.crypto-sig-cache"),
|
||||
true,
|
||||
));
|
||||
crate::crypto_sig_cache::install_global(std::sync::Arc::clone(&cache));
|
||||
|
||||
// Miss -> real verification runs and passes -> positive conclusion written.
|
||||
verify_child_signature(&child_cert, &issuer_cert.tbs_certificate.subject_pki)
|
||||
.expect("signature ok");
|
||||
// Hit -> skipped (a second execution would also pass, so assert via stats).
|
||||
verify_child_signature(&child_cert, &issuer_cert.tbs_certificate.subject_pki)
|
||||
.expect("signature ok");
|
||||
// Wrong issuer: real verification fails, nothing is written (no negative caching),
|
||||
// so a retry executes the real verification again instead of inheriting anything.
|
||||
for _ in 0..2 {
|
||||
let err = verify_child_signature(&child_cert, &other_cert.tbs_certificate.subject_pki)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CaPathError::ChildSignatureInvalid(_)),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
let summary = cache.summary();
|
||||
let stats = summary
|
||||
.per_point
|
||||
.get("child_ca_cert")
|
||||
.expect("child_ca_cert stats");
|
||||
assert_eq!(stats.calls, 4);
|
||||
assert_eq!(stats.would_hit, 1);
|
||||
assert_eq!(stats.new_keys, 3);
|
||||
assert_eq!(stats.verify_executed, 3);
|
||||
assert_eq!(stats.verify_skipped, 1);
|
||||
crate::crypto_sig_cache::clear_global();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issuer_effective_resources_index_and_indexed_resolvers_cover_success_and_failure_paths() {
|
||||
use crate::data_model::rc::{AsIdOrRange, IpPrefix};
|
||||
|
||||
@@ -385,8 +385,16 @@ fn verify_ee_cert_signature(
|
||||
ee: &X509Certificate<'_>,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
) -> Result<(), CertPathError> {
|
||||
ee.verify_signature(Some(issuer_spki))
|
||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
||||
crate::crypto_sig_cache::verify_with_cache(
|
||||
crate::crypto_sig_cache::CryptoSigVerifyPoint::EeCertX509,
|
||||
ee.tbs_certificate.as_ref(),
|
||||
ee.signature_value.data.as_ref(),
|
||||
issuer_spki.raw,
|
||||
|| {
|
||||
ee.verify_signature(Some(issuer_spki))
|
||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_ee_cert_signature_fast(
|
||||
@@ -394,12 +402,20 @@ fn verify_ee_cert_signature_fast(
|
||||
ee_signature_bytes: &[u8],
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
) -> Result<(), CertPathError> {
|
||||
let key = signature::UnparsedPublicKey::new(
|
||||
&signature::RSA_PKCS1_2048_8192_SHA256,
|
||||
&issuer_spki.subject_public_key.data,
|
||||
);
|
||||
key.verify(ee_tbs_der, ee_signature_bytes)
|
||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
||||
crate::crypto_sig_cache::verify_with_cache(
|
||||
crate::crypto_sig_cache::CryptoSigVerifyPoint::EeCertFast,
|
||||
ee_tbs_der,
|
||||
ee_signature_bytes,
|
||||
issuer_spki.raw,
|
||||
|| {
|
||||
let key = signature::UnparsedPublicKey::new(
|
||||
&signature::RSA_PKCS1_2048_8192_SHA256,
|
||||
&issuer_spki.subject_public_key.data,
|
||||
);
|
||||
key.verify(ee_tbs_der, ee_signature_bytes)
|
||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn validate_ee_aki_matches_issuer_ski(
|
||||
@@ -720,4 +736,145 @@ mod tests {
|
||||
validate_ee_crldp_contains_issuer_crl_uri(&ee, "rsync://example.test/issuer.crl")
|
||||
.expect("crldp ok");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signature_cache_hit_does_not_skip_crl_revocation_check() {
|
||||
use crate::crypto_sig_cache::{
|
||||
CryptoSigCache, CryptoSigVerifyPoint, compute_cache_key,
|
||||
};
|
||||
use crate::data_model::common::{Asn1TimeEncoding, Asn1TimeUtc};
|
||||
use crate::data_model::crl::CrlExtensions;
|
||||
|
||||
let _guard = crate::crypto_sig_cache::GLOBAL_TEST_LOCK.lock().expect("test lock");
|
||||
crate::crypto_sig_cache::clear_global();
|
||||
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let mut issuer = dummy_cert(
|
||||
ResourceCertKind::Ca,
|
||||
"CN=issuer",
|
||||
"CN=issuer",
|
||||
Some(vec![1]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
issuer.tbs.validity_not_before = now - time::Duration::days(1);
|
||||
issuer.tbs.validity_not_after = now + time::Duration::days(1);
|
||||
let mut ee_rc = dummy_cert(
|
||||
ResourceCertKind::Ee,
|
||||
"CN=ee",
|
||||
"CN=issuer",
|
||||
Some(vec![2]),
|
||||
Some(vec![1]),
|
||||
Some(vec!["rsync://example.test/issuer.cer"]),
|
||||
Some(vec!["rsync://example.test/issuer.crl"]),
|
||||
);
|
||||
ee_rc.tbs.validity_not_before = now - time::Duration::days(1);
|
||||
ee_rc.tbs.validity_not_after = now + time::Duration::days(1);
|
||||
|
||||
// Tampered "object": these bytes could never pass the real RSA verification, so an
|
||||
// Ok outcome below proves the crypto step was skipped by the cache hit.
|
||||
let ee = ResourceEeCertificate {
|
||||
raw_der: Vec::new(),
|
||||
subject_key_identifier: vec![2],
|
||||
spki_der: Vec::new(),
|
||||
rsa_public_modulus: Vec::new(),
|
||||
rsa_public_exponent: Vec::new(),
|
||||
tbs_certificate_der: b"tampered-tbs-bytes".to_vec(),
|
||||
signature_bytes: b"tampered-signature-bytes".to_vec(),
|
||||
key_usage_summary: EeKeyUsageSummary::DigitalSignatureOnly,
|
||||
sia_signed_object_uris: Vec::new(),
|
||||
resource_cert: ee_rc,
|
||||
};
|
||||
|
||||
// Any real RSA SPKI works as the verifier identity (the TA fixture is a valid RSA
|
||||
// certificate); the SPKI DER only feeds the cache key here.
|
||||
let ta_der = std::fs::read(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/tests/fixtures/ta/apnic-ta.cer"
|
||||
))
|
||||
.expect("read ta fixture");
|
||||
let (_, ta_x509) = X509Certificate::from_der(&ta_der).expect("parse ta fixture");
|
||||
let issuer_spki = &ta_x509.tbs_certificate.subject_pki;
|
||||
|
||||
let crl = RpkixCrl {
|
||||
raw_der: Vec::new(),
|
||||
version: 2,
|
||||
issuer_dn: "CN=issuer".to_string(),
|
||||
signature_algorithm_oid: "1.2.840.113549.1.1.11".to_string(),
|
||||
this_update: Asn1TimeUtc {
|
||||
utc: now - time::Duration::hours(1),
|
||||
encoding: Asn1TimeEncoding::UtcTime,
|
||||
},
|
||||
next_update: Asn1TimeUtc {
|
||||
utc: now + time::Duration::hours(1),
|
||||
encoding: Asn1TimeEncoding::UtcTime,
|
||||
},
|
||||
revoked_certs: Vec::new(),
|
||||
extensions: CrlExtensions {
|
||||
authority_key_identifier: vec![1],
|
||||
crl_number: BigUnsigned { bytes_be: vec![1] },
|
||||
},
|
||||
};
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let cache = std::sync::Arc::new(CryptoSigCache::load_or_rebuild(
|
||||
dir.path().join("work-db.crypto-sig-cache"),
|
||||
true,
|
||||
));
|
||||
cache.insert_positive(compute_cache_key(
|
||||
CryptoSigVerifyPoint::EeCertFast,
|
||||
b"tampered-tbs-bytes",
|
||||
b"tampered-signature-bytes",
|
||||
issuer_spki.raw,
|
||||
));
|
||||
crate::crypto_sig_cache::install_global(std::sync::Arc::clone(&cache));
|
||||
|
||||
// EE serial is 1 (dummy_cert); revoke it. The revocation check runs before the
|
||||
// signature step in this code path, so the result must be EeRevoked — never Ok —
|
||||
// even though a positive signature conclusion is cached.
|
||||
let revoked_serials: HashSet<Vec<u8>> = HashSet::from([vec![1u8]]);
|
||||
let err = validate_signed_object_ee_cert_path_fast(
|
||||
&ee,
|
||||
&issuer,
|
||||
issuer_spki,
|
||||
&crl,
|
||||
&revoked_serials,
|
||||
Some("rsync://example.test/issuer.cer"),
|
||||
Some("rsync://example.test/issuer.crl"),
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, CertPathError::EeRevoked),
|
||||
"cached signature conclusion must not bypass revocation: {err}"
|
||||
);
|
||||
|
||||
// Not revoked: the cached positive conclusion lets validation pass even though the
|
||||
// tampered bytes could never verify — proving the crypto step was skipped.
|
||||
let not_revoked: HashSet<Vec<u8>> = HashSet::new();
|
||||
validate_signed_object_ee_cert_path_fast(
|
||||
&ee,
|
||||
&issuer,
|
||||
issuer_spki,
|
||||
&crl,
|
||||
¬_revoked,
|
||||
Some("rsync://example.test/issuer.cer"),
|
||||
Some("rsync://example.test/issuer.crl"),
|
||||
now,
|
||||
)
|
||||
.expect("cache hit skips the impossible verification");
|
||||
|
||||
let summary = cache.summary();
|
||||
let stats = summary
|
||||
.per_point
|
||||
.get("ee_cert_fast")
|
||||
.expect("ee_cert_fast stats");
|
||||
assert_eq!(stats.verify_skipped, 1);
|
||||
assert_eq!(
|
||||
stats.verify_executed, 0,
|
||||
"real verification must never run on cache hits"
|
||||
);
|
||||
crate::crypto_sig_cache::clear_global();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user