20260722 新增密码学签名验证缓存(#126):5个验签收口点接入独立缓存,observe-only/enable双开关默认关闭,消融脚本新增crypto-sig等case set,verification contract补crypto_signature字段

This commit is contained in:
2026-07-22 12:14:13 +08:00
parent f86fbd4939
commit 3d772f748c
11 changed files with 1313 additions and 22 deletions

View File

@@ -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,
&not_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();
}
}