20260722 新增密码学签名验证缓存(#126):5个验签收口点接入独立缓存,observe-only/enable双开关默认关闭,消融脚本新增crypto-sig等case set,verification contract补crypto_signature字段
This commit is contained in:
parent
f86fbd4939
commit
3d772f748c
@ -38,7 +38,7 @@ Environment:
|
||||
EXPERIMENT_RUN_ROOT shared run root/state root; default PACKAGE_ROOT
|
||||
EXPERIMENT_DIR experiment metadata output directory
|
||||
CASE_RUNS delta runs per case; default 10
|
||||
EXPERIMENT_CASE_SET default or cache-only; default runs the original 4-case matrix
|
||||
EXPERIMENT_CASE_SET default, cache-only, crypto-sig or sig-pp-compare; default runs the original 4-case matrix
|
||||
RUN_START_INTERVAL_SECS fixed start cadence for all runs; default 600
|
||||
FIRST_RUN_DELAY_SECS delay before the first scheduled run; default 0
|
||||
SNAPSHOT_EXTRA_ARGS extra rpki args for snapshot warmup
|
||||
@ -95,7 +95,10 @@ case_count() {
|
||||
case "$EXPERIMENT_CASE_SET" in
|
||||
default) printf '%s' 4 ;;
|
||||
cache-only) printf '%s' 3 ;;
|
||||
*) die "EXPERIMENT_CASE_SET must be default or cache-only: $EXPERIMENT_CASE_SET" ;;
|
||||
crypto-sig) printf '%s' 4 ;;
|
||||
sig-pp-compare) printf '%s' 2 ;;
|
||||
baseline-combo) printf '%s' 2 ;;
|
||||
*) die "EXPERIMENT_CASE_SET must be default, cache-only, crypto-sig or sig-pp-compare: $EXPERIMENT_CASE_SET" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@ -108,6 +111,14 @@ case_id_for_index() {
|
||||
cache-only:1) printf '%s' "pp-only" ;;
|
||||
cache-only:2) printf '%s' "object-only" ;;
|
||||
cache-only:3) printf '%s' "pp-object-only" ;;
|
||||
crypto-sig:1) printf '%s' "baseline" ;;
|
||||
crypto-sig:2) printf '%s' "sig-only" ;;
|
||||
crypto-sig:3) printf '%s' "pp-object" ;;
|
||||
crypto-sig:4) printf '%s' "pp-object-sig" ;;
|
||||
sig-pp-compare:1) printf '%s' "sig-only" ;;
|
||||
sig-pp-compare:2) printf '%s' "pp-only" ;;
|
||||
baseline-combo:1) printf '%s' "baseline" ;;
|
||||
baseline-combo:2) printf '%s' "pp-object-sig" ;;
|
||||
*) die "unknown case index: $1 for set $EXPERIMENT_CASE_SET" ;;
|
||||
esac
|
||||
}
|
||||
@ -121,6 +132,14 @@ case_name_for_index() {
|
||||
cache-only:1) printf '%s' "pp-cache-only" ;;
|
||||
cache-only:2) printf '%s' "object-cache-only" ;;
|
||||
cache-only:3) printf '%s' "pp-cache-object-cache-only" ;;
|
||||
crypto-sig:1) printf '%s' "all-cache-off" ;;
|
||||
crypto-sig:2) printf '%s' "crypto-sig-cache-only" ;;
|
||||
crypto-sig:3) printf '%s' "pp-cache-object-cache" ;;
|
||||
crypto-sig:4) printf '%s' "pp-cache-object-crypto-sig-cache" ;;
|
||||
sig-pp-compare:1) printf '%s' "crypto-sig-cache-only" ;;
|
||||
sig-pp-compare:2) printf '%s' "pp-cache-only" ;;
|
||||
baseline-combo:1) printf '%s' "all-cache-off" ;;
|
||||
baseline-combo:2) printf '%s' "pp-cache-object-crypto-sig-cache" ;;
|
||||
*) die "unknown case index: $1 for set $EXPERIMENT_CASE_SET" ;;
|
||||
esac
|
||||
}
|
||||
@ -134,14 +153,22 @@ case_extra_args_for_index() {
|
||||
cache-only:1) printf '%s' "--enable-publication-point-validation-cache" ;;
|
||||
cache-only:2) printf '%s' "--enable-roa-validation-cache" ;;
|
||||
cache-only:3) printf '%s' "--enable-publication-point-validation-cache --enable-roa-validation-cache" ;;
|
||||
crypto-sig:1) printf '%s' "" ;;
|
||||
crypto-sig:2) printf '%s' "--enable-crypto-signature-cache" ;;
|
||||
crypto-sig:3) printf '%s' "--enable-publication-point-validation-cache --enable-roa-validation-cache" ;;
|
||||
crypto-sig:4) printf '%s' "--enable-publication-point-validation-cache --enable-roa-validation-cache --enable-crypto-signature-cache" ;;
|
||||
sig-pp-compare:1) printf '%s' "--enable-crypto-signature-cache" ;;
|
||||
sig-pp-compare:2) printf '%s' "--enable-publication-point-validation-cache" ;;
|
||||
baseline-combo:1) printf '%s' "" ;;
|
||||
baseline-combo:2) printf '%s' "--enable-publication-point-validation-cache --enable-roa-validation-cache --enable-crypto-signature-cache" ;;
|
||||
*) die "unknown case index: $1 for set $EXPERIMENT_CASE_SET" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
case_child_cert_cache_for_index() {
|
||||
case "$EXPERIMENT_CASE_SET:$1" in
|
||||
default:4|cache-only:2|cache-only:3) printf '%s' "1" ;;
|
||||
default:1|default:2|default:3|cache-only:1) printf '%s' "0" ;;
|
||||
default:4|cache-only:2|cache-only:3|crypto-sig:3|crypto-sig:4|baseline-combo:2) printf '%s' "1" ;;
|
||||
default:1|default:2|default:3|cache-only:1|crypto-sig:1|crypto-sig:2|sig-pp-compare:1|sig-pp-compare:2|baseline-combo:1) printf '%s' "0" ;;
|
||||
*) die "unknown case index: $1 for set $EXPERIMENT_CASE_SET" ;;
|
||||
esac
|
||||
}
|
||||
@ -163,6 +190,23 @@ elif case_set == "cache-only":
|
||||
{"caseId": "object-only", "caseName": "object-cache-only", "extraArgs": "--enable-roa-validation-cache", "enableChildCertificateValidationCache": True},
|
||||
{"caseId": "pp-object-only", "caseName": "pp-cache-object-cache-only", "extraArgs": "--enable-publication-point-validation-cache --enable-roa-validation-cache", "enableChildCertificateValidationCache": True},
|
||||
]
|
||||
elif case_set == "crypto-sig":
|
||||
cases = [
|
||||
{"caseId": "baseline", "caseName": "all-cache-off", "extraArgs": "", "enableChildCertificateValidationCache": False},
|
||||
{"caseId": "sig-only", "caseName": "crypto-sig-cache-only", "extraArgs": "--enable-crypto-signature-cache", "enableChildCertificateValidationCache": False},
|
||||
{"caseId": "pp-object", "caseName": "pp-cache-object-cache", "extraArgs": "--enable-publication-point-validation-cache --enable-roa-validation-cache", "enableChildCertificateValidationCache": True},
|
||||
{"caseId": "pp-object-sig", "caseName": "pp-cache-object-crypto-sig-cache", "extraArgs": "--enable-publication-point-validation-cache --enable-roa-validation-cache --enable-crypto-signature-cache", "enableChildCertificateValidationCache": True},
|
||||
]
|
||||
elif case_set == "sig-pp-compare":
|
||||
cases = [
|
||||
{"caseId": "sig-only", "caseName": "crypto-sig-cache-only", "extraArgs": "--enable-crypto-signature-cache", "enableChildCertificateValidationCache": False},
|
||||
{"caseId": "pp-only", "caseName": "pp-cache-only", "extraArgs": "--enable-publication-point-validation-cache", "enableChildCertificateValidationCache": False},
|
||||
]
|
||||
elif case_set == "baseline-combo":
|
||||
cases = [
|
||||
{"caseId": "baseline", "caseName": "all-cache-off", "extraArgs": "", "enableChildCertificateValidationCache": False},
|
||||
{"caseId": "pp-object-sig", "caseName": "pp-cache-object-crypto-sig-cache", "extraArgs": "--enable-publication-point-validation-cache --enable-roa-validation-cache --enable-crypto-signature-cache", "enableChildCertificateValidationCache": True},
|
||||
]
|
||||
else:
|
||||
raise SystemExit(f"unknown case set: {case_set}")
|
||||
print(json.dumps(cases, ensure_ascii=False, indent=8))
|
||||
@ -345,6 +389,7 @@ else:
|
||||
"publication_point_cache_index_load",
|
||||
"publication_point_cache_index_refresh",
|
||||
"roa_validation_cache",
|
||||
"crypto_signature_cache_observe",
|
||||
]:
|
||||
if key in stage:
|
||||
record[key] = stage.get(key)
|
||||
@ -360,6 +405,11 @@ else:
|
||||
"child_certificate_cache_miss_not_found",
|
||||
"fresh_publication_points",
|
||||
"fresh_manifest_files_total",
|
||||
"crypto_signature_cache_observe_calls",
|
||||
"crypto_signature_cache_observe_would_hit",
|
||||
"crypto_signature_cache_observe_new_keys",
|
||||
"crypto_signature_cache_verify_executed",
|
||||
"crypto_signature_cache_verify_skipped",
|
||||
]
|
||||
record["cacheCounts"] = {k: analysis_counts.get(k) for k in interesting if k in analysis_counts}
|
||||
record["repoSyncStats"] = summary.get("repoSyncStats")
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
- `vrps.csv`、`vaps.csv`
|
||||
- `stage-timing.json`、`validation-events.jsonl`
|
||||
- `validation-contract.json`、`verification-meta.json`
|
||||
- `compare/ccr-state-compare.json`、`compare/ccr-state-compare.md`
|
||||
- `compare/ccr-state-digest.json`、`compare/ccr-state-digest.md`
|
||||
- `stdout.log`、`stderr.log`、`process-time.txt`、`wrapper-timing.json`
|
||||
|
||||
通过条件是 compare JSON 中 `stateDigestMatch=true`。`producedAt` 不参与比较。
|
||||
|
||||
68
src/cli.rs
68
src/cli.rs
@ -55,6 +55,7 @@ struct RunStageTiming {
|
||||
enable_child_certificate_validation_cache: bool,
|
||||
publication_point_cache_observe_only: bool,
|
||||
enable_publication_point_validation_cache: bool,
|
||||
crypto_signature_cache_observe: Option<crate::crypto_sig_cache::CryptoSigCacheSummary>,
|
||||
enable_transport_request_prefetch: bool,
|
||||
report_build_ms: u64,
|
||||
report_write_ms: Option<u64>,
|
||||
@ -145,6 +146,8 @@ pub struct CliArgs {
|
||||
pub enable_child_certificate_validation_cache: bool,
|
||||
pub publication_point_cache_observe_only: bool,
|
||||
pub enable_publication_point_validation_cache: bool,
|
||||
pub crypto_signature_cache_observe_only: bool,
|
||||
pub enable_crypto_signature_cache: bool,
|
||||
pub enable_transport_request_prefetch: bool,
|
||||
pub ccr_out_path: Option<PathBuf>,
|
||||
pub vrps_csv_out_path: Option<PathBuf>,
|
||||
@ -222,6 +225,12 @@ Options:
|
||||
Evaluate publication-point cache eligibility without changing results
|
||||
--enable-publication-point-validation-cache
|
||||
Experimental: reuse complete publication-point validation projections
|
||||
--crypto-signature-cache-observe-only
|
||||
Measure crypto signature cache key hit rates and verify durations
|
||||
without changing validation behavior (default: off)
|
||||
--enable-crypto-signature-cache
|
||||
Experimental: skip cryptographic signature verification on cache
|
||||
hits (positive conclusions only; default: off)
|
||||
--enable-transport-request-prefetch
|
||||
Experimental: prefetch previous run transport repo requests before tree traversal
|
||||
--ccr-out <path> Write CCR DER ContentInfo to this path (optional)
|
||||
@ -315,6 +324,8 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
||||
let mut enable_roa_validation_cache: bool = false;
|
||||
let mut enable_child_certificate_validation_cache: bool = false;
|
||||
let mut publication_point_cache_observe_only: bool = false;
|
||||
let mut crypto_signature_cache_observe_only: bool = false;
|
||||
let mut enable_crypto_signature_cache: bool = false;
|
||||
let mut enable_publication_point_validation_cache: bool = false;
|
||||
let mut enable_transport_request_prefetch: bool = false;
|
||||
let mut ccr_out_path: Option<PathBuf> = None;
|
||||
@ -579,6 +590,12 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
||||
"--enable-publication-point-validation-cache" => {
|
||||
enable_publication_point_validation_cache = true;
|
||||
}
|
||||
"--crypto-signature-cache-observe-only" => {
|
||||
crypto_signature_cache_observe_only = true;
|
||||
}
|
||||
"--enable-crypto-signature-cache" => {
|
||||
enable_crypto_signature_cache = true;
|
||||
}
|
||||
"--enable-transport-request-prefetch" => {
|
||||
enable_transport_request_prefetch = true;
|
||||
}
|
||||
@ -808,6 +825,8 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
||||
|| enable_child_certificate_validation_cache
|
||||
|| publication_point_cache_observe_only
|
||||
|| enable_publication_point_validation_cache
|
||||
|| crypto_signature_cache_observe_only
|
||||
|| enable_crypto_signature_cache
|
||||
|| enable_transport_request_prefetch
|
||||
|| rsync_local_dir.is_some()
|
||||
|| rsync_command.is_some()
|
||||
@ -1121,6 +1140,8 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
||||
enable_child_certificate_validation_cache,
|
||||
publication_point_cache_observe_only,
|
||||
enable_publication_point_validation_cache,
|
||||
crypto_signature_cache_observe_only,
|
||||
enable_crypto_signature_cache,
|
||||
enable_transport_request_prefetch,
|
||||
ccr_out_path,
|
||||
vrps_csv_out_path,
|
||||
@ -2176,6 +2197,8 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
||||
args.enable_child_certificate_validation_cache = false;
|
||||
args.publication_point_cache_observe_only = false;
|
||||
args.enable_publication_point_validation_cache = false;
|
||||
args.crypto_signature_cache_observe_only = false;
|
||||
args.enable_crypto_signature_cache = false;
|
||||
args.enable_transport_request_prefetch = false;
|
||||
args.ccr_out_path = Some(prepared.artifacts.result_ccr.clone());
|
||||
args.vrps_csv_out_path = Some(prepared.artifacts.vrps_csv.clone());
|
||||
@ -2231,6 +2254,7 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
||||
roa: args.enable_roa_validation_cache,
|
||||
child_certificate: args.enable_child_certificate_validation_cache,
|
||||
transport_prefetch: args.enable_transport_request_prefetch,
|
||||
crypto_signature: args.enable_crypto_signature_cache,
|
||||
},
|
||||
)?;
|
||||
crate::verification_only::write_validation_contract(path, &contract)?;
|
||||
@ -2351,6 +2375,19 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
||||
store.as_ref(),
|
||||
);
|
||||
let validation_started = std::time::Instant::now();
|
||||
let crypto_sig_cache = if args.crypto_signature_cache_observe_only
|
||||
|| args.enable_crypto_signature_cache
|
||||
{
|
||||
let cache_file = crate::crypto_sig_cache::default_cache_file_path(&args.db_path);
|
||||
let cache = Arc::new(crate::crypto_sig_cache::CryptoSigCache::load_or_rebuild(
|
||||
cache_file,
|
||||
args.enable_crypto_signature_cache,
|
||||
));
|
||||
crate::crypto_sig_cache::install_global(Arc::clone(&cache));
|
||||
Some(cache)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let collect_current_repo_objects = false;
|
||||
let out = if args.verification_only {
|
||||
let http = crate::verification_only::NetworkDisabledHttpFetcher;
|
||||
@ -2778,6 +2815,36 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
||||
None
|
||||
};
|
||||
let publication_point_cache_index_load = store.publication_point_cache_mmap_index_load_stats();
|
||||
let crypto_signature_cache_observe = crypto_sig_cache.as_ref().map(|cache| {
|
||||
if let Err(e) = cache.persist() {
|
||||
crate::progress_log::emit(
|
||||
"crypto_signature_cache_persist",
|
||||
serde_json::json!({
|
||||
"state": "failed",
|
||||
"error": e,
|
||||
}),
|
||||
);
|
||||
}
|
||||
crate::crypto_sig_cache::clear_global();
|
||||
cache.summary()
|
||||
});
|
||||
if let (Some((_, t)), Some(summary)) = (timing.as_ref(), crypto_signature_cache_observe.as_ref())
|
||||
{
|
||||
let (mut calls, mut would_hit, mut new_keys, mut executed, mut skipped) =
|
||||
(0u64, 0u64, 0u64, 0u64, 0u64);
|
||||
for stats in summary.per_point.values() {
|
||||
calls += stats.calls;
|
||||
would_hit += stats.would_hit;
|
||||
new_keys += stats.new_keys;
|
||||
executed += stats.verify_executed;
|
||||
skipped += stats.verify_skipped;
|
||||
}
|
||||
t.record_count("crypto_signature_cache_observe_calls", calls);
|
||||
t.record_count("crypto_signature_cache_observe_would_hit", would_hit);
|
||||
t.record_count("crypto_signature_cache_observe_new_keys", new_keys);
|
||||
t.record_count("crypto_signature_cache_verify_executed", executed);
|
||||
t.record_count("crypto_signature_cache_verify_skipped", skipped);
|
||||
}
|
||||
let timing_report_snapshot = timing
|
||||
.as_ref()
|
||||
.map(|(_, handle)| handle.report_snapshot(50));
|
||||
@ -2787,6 +2854,7 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
||||
enable_child_certificate_validation_cache: args.enable_child_certificate_validation_cache,
|
||||
publication_point_cache_observe_only: args.publication_point_cache_observe_only,
|
||||
enable_publication_point_validation_cache: args.enable_publication_point_validation_cache,
|
||||
crypto_signature_cache_observe,
|
||||
enable_transport_request_prefetch: args.enable_transport_request_prefetch,
|
||||
report_build_ms,
|
||||
report_write_ms,
|
||||
|
||||
@ -289,6 +289,56 @@ fn parse_accepts_publication_point_cache_flags() {
|
||||
assert!(args.enable_publication_point_validation_cache);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_disables_crypto_signature_cache_observe_only_by_default() {
|
||||
let argv = vec![
|
||||
"rpki".to_string(),
|
||||
"--db".to_string(),
|
||||
"db".to_string(),
|
||||
"--tal-path".to_string(),
|
||||
"x.tal".to_string(),
|
||||
"--ta-path".to_string(),
|
||||
"x.cer".to_string(),
|
||||
];
|
||||
let args = parse_args(&argv).expect("parse args");
|
||||
assert!(!args.crypto_signature_cache_observe_only);
|
||||
assert!(!args.enable_crypto_signature_cache);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_accepts_crypto_signature_cache_observe_only() {
|
||||
let argv = vec![
|
||||
"rpki".to_string(),
|
||||
"--db".to_string(),
|
||||
"db".to_string(),
|
||||
"--tal-path".to_string(),
|
||||
"x.tal".to_string(),
|
||||
"--ta-path".to_string(),
|
||||
"x.cer".to_string(),
|
||||
"--crypto-signature-cache-observe-only".to_string(),
|
||||
];
|
||||
let args = parse_args(&argv).expect("parse args");
|
||||
assert!(args.crypto_signature_cache_observe_only);
|
||||
assert!(!args.enable_crypto_signature_cache);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_accepts_enable_crypto_signature_cache() {
|
||||
let argv = vec![
|
||||
"rpki".to_string(),
|
||||
"--db".to_string(),
|
||||
"db".to_string(),
|
||||
"--tal-path".to_string(),
|
||||
"x.tal".to_string(),
|
||||
"--ta-path".to_string(),
|
||||
"x.cer".to_string(),
|
||||
"--enable-crypto-signature-cache".to_string(),
|
||||
];
|
||||
let args = parse_args(&argv).expect("parse args");
|
||||
assert!(args.enable_crypto_signature_cache);
|
||||
assert!(!args.crypto_signature_cache_observe_only);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_accepts_enable_transport_request_prefetch() {
|
||||
let argv = vec![
|
||||
@ -1807,6 +1857,7 @@ fn run_report_task_and_stage_timing_work() {
|
||||
enable_child_certificate_validation_cache: false,
|
||||
publication_point_cache_observe_only: false,
|
||||
enable_publication_point_validation_cache: false,
|
||||
crypto_signature_cache_observe: None,
|
||||
enable_transport_request_prefetch: false,
|
||||
report_build_ms: report_output.report_build_ms,
|
||||
report_write_ms: report_output.report_write_ms,
|
||||
@ -1913,6 +1964,7 @@ fn stage_timing_serializes_memory_telemetry() {
|
||||
enable_child_certificate_validation_cache: true,
|
||||
publication_point_cache_observe_only: false,
|
||||
enable_publication_point_validation_cache: false,
|
||||
crypto_signature_cache_observe: None,
|
||||
enable_transport_request_prefetch: true,
|
||||
report_build_ms: 2,
|
||||
report_write_ms: None,
|
||||
|
||||
876
src/crypto_sig_cache.rs
Normal file
876
src/crypto_sig_cache.rs
Normal file
@ -0,0 +1,876 @@
|
||||
//! Crypto signature verification cache (feature #126).
|
||||
//!
|
||||
//! Caches the context-free cryptographic fact "this signature over these exact bytes verifies
|
||||
//! under this public key" so that later runs can skip the RSA verification entirely. Only
|
||||
//! positive conclusions are stored (no negative caching). The cache never replaces any other
|
||||
//! validation semantics: revocation (CRL), time-window, and policy checks all run unchanged on
|
||||
//! the hit path — the hook wraps exactly the crypto verify call and nothing else.
|
||||
//!
|
||||
//! Modes (CLI):
|
||||
//!
|
||||
//! - `--crypto-signature-cache-observe-only` (M1): compute keys, maintain the persistent set,
|
||||
//! record hit/miss/timing statistics, always run the real verification.
|
||||
//! - `--enable-crypto-signature-cache` (M2): on a cache hit, skip the real verification and
|
||||
//! return success; on a miss, run the real verification and store the positive conclusion.
|
||||
//! Statistics are recorded the same way; `verify_skipped`/`verify_executed` report what
|
||||
//! actually happened. Both flags may be combined (reuse + statistics).
|
||||
//!
|
||||
//! The five choke points (see the development plan for the full caller inventory):
|
||||
//!
|
||||
//! - `CmsSignedObject`: `data_model::signed_object` CMS verify (ROA / ASPA / manifest on the
|
||||
//! main validation path; GBR objects are not signature-verified by the tree runner).
|
||||
//! - `EeCertFast`: `validation::cert_path::verify_ee_cert_signature_fast` (ring, TBS + signature
|
||||
//! bytes from the signed-object embedded EE certificate; ROA / ASPA / manifest EE paths).
|
||||
//! - `EeCertX509`: `validation::cert_path::verify_ee_cert_signature` (x509-parser; router
|
||||
//! certificates and the `validate_ee_cert_path` family).
|
||||
//! - `ChildCaCert`: `validation::ca_path::verify_child_signature` (x509-parser; subordinate CA
|
||||
//! certificates).
|
||||
//! - `Crl`: `data_model::crl::verify_signature_with_issuer_spki` (x509-parser; issuer CRLs).
|
||||
//!
|
||||
//! Trust-anchor self-signature verification is deliberately excluded (a handful of calls per
|
||||
//! run).
|
||||
//!
|
||||
//! Cache key (M1 decision: verify-input bytes, not full object bytes — the key material is in
|
||||
//! hand inside each choke point, while the repo-bytes content hash is only available at the
|
||||
//! upper processing layer):
|
||||
//!
|
||||
//! ```text
|
||||
//! key = SHA-256( "rpki-crypto-sig-cache-key-v1" | point-tag | "sha256WithRSAEncryption"
|
||||
//! | SHA-256(verify input bytes) | SHA-256(signature bytes)
|
||||
//! | SHA-256(verifier SPKI DER) )
|
||||
//! ```
|
||||
//!
|
||||
//! The signature bytes are part of the key so that a different signature over the same input
|
||||
//! under the same key can never inherit a positive verification fact.
|
||||
//!
|
||||
//! Storage:
|
||||
//!
|
||||
//! - Single small file next to the work DB (`<work-db-name>.crypto-sig-cache`), never inside
|
||||
//! the work DB, not touched by periodic snapshot reset (#091/#092): the reset paths only
|
||||
//! remove/replace the work DB directory and the PP/child-cert cache artifacts, not siblings
|
||||
//! of their own choosing.
|
||||
//! - Binary format: 8-byte magic `RPKICSC2` | u64 record-count | records of
|
||||
//! key[32] | verified_at_unix_secs(i64) | crypto_impl_version(u32) | reserved(u32).
|
||||
//! A missing, corrupt, or magic-mismatched file (including the M1 `RPKICSC1` seen-set
|
||||
//! format) is silently rebuilt empty. Writes are atomic (write-then-rename).
|
||||
//! - The cache is persisted once at the end of the validation stage (same point as the M1
|
||||
//! seen-set). If a run fails mid-validation, entries added during that run are lost;
|
||||
//! previously persisted entries are unaffected.
|
||||
//! - Size cap: approximate上限 `capacity` entries (default 2,000,000; measured M1 APNIC
|
||||
//! ~148k keys/run, all5 estimated ~750k). When an insert would exceed the cap, the whole
|
||||
//! cache is cleared and rebuilt from scratch (full-rebuild eviction — the simplest policy;
|
||||
//! concurrent inserts may briefly overshoot the cap by a few entries). Each eviction is
|
||||
//! counted in `evictions_total`.
|
||||
//! - Concurrency: entries live in 64 shards keyed by the first key byte, so parallel
|
||||
//! validation workers rarely contend; statistics use relaxed atomics.
|
||||
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
|
||||
use std::sync::{Arc, Mutex, RwLock};
|
||||
|
||||
use serde::Serialize;
|
||||
use sha2::Digest;
|
||||
|
||||
const CACHE_FILE_MAGIC: &[u8; 8] = b"RPKICSC2";
|
||||
const KEY_DOMAIN: &[u8] = b"rpki-crypto-sig-cache-key-v1";
|
||||
const ALG_ID: &[u8] = b"sha256WithRSAEncryption";
|
||||
const SHARD_COUNT: usize = 64;
|
||||
const RECORD_BYTES: usize = 32 + 8 + 4 + 4;
|
||||
|
||||
/// The schema version reported in summaries. The on-disk magic already carries the format
|
||||
/// version; this constant is the semantic version of the key/value definitions.
|
||||
pub const CACHE_SCHEMA_VERSION: u32 = 2;
|
||||
|
||||
/// Default maximum number of cached positive conclusions (see module docs).
|
||||
pub const DEFAULT_CAPACITY: usize = 2_000_000;
|
||||
|
||||
/// Crypto implementation identifier stored in every value. Bump when the verification
|
||||
/// backend changes so old entries can be invalidated wholesale.
|
||||
/// `1` = ring 0.17 `RSA_PKCS1_2048_8192_SHA256` (direct ring calls and x509-parser's
|
||||
/// `verify` feature, which is backed by ring in this crate).
|
||||
pub const CRYPTO_IMPL_VERSION: u32 = 1;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum CryptoSigVerifyPoint {
|
||||
CmsSignedObject,
|
||||
EeCertFast,
|
||||
EeCertX509,
|
||||
ChildCaCert,
|
||||
Crl,
|
||||
}
|
||||
|
||||
impl CryptoSigVerifyPoint {
|
||||
pub const ALL: [Self; 5] = [
|
||||
Self::CmsSignedObject,
|
||||
Self::EeCertFast,
|
||||
Self::EeCertX509,
|
||||
Self::ChildCaCert,
|
||||
Self::Crl,
|
||||
];
|
||||
|
||||
fn tag(self) -> u8 {
|
||||
match self {
|
||||
Self::CmsSignedObject => 1,
|
||||
Self::EeCertFast => 2,
|
||||
Self::EeCertX509 => 3,
|
||||
Self::ChildCaCert => 4,
|
||||
Self::Crl => 5,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::CmsSignedObject => "cms_signed_object",
|
||||
Self::EeCertFast => "ee_cert_fast",
|
||||
Self::EeCertX509 => "ee_cert_x509",
|
||||
Self::ChildCaCert => "child_ca_cert",
|
||||
Self::Crl => "crl",
|
||||
}
|
||||
}
|
||||
|
||||
fn from_index(index: usize) -> Self {
|
||||
Self::ALL[index]
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the cache key for one verification fact.
|
||||
///
|
||||
/// `input_bytes` is the exact byte string fed to the verifier (CMS signedAttrs DER for
|
||||
/// signature, TBSCertificate DER, TBSCertList DER), `signature_bytes` the signature value,
|
||||
/// and `verifier_spki_der` the DER of the verifying SubjectPublicKeyInfo.
|
||||
pub fn compute_cache_key(
|
||||
point: CryptoSigVerifyPoint,
|
||||
input_bytes: &[u8],
|
||||
signature_bytes: &[u8],
|
||||
verifier_spki_der: &[u8],
|
||||
) -> [u8; 32] {
|
||||
let mut h = sha2::Sha256::new();
|
||||
h.update(KEY_DOMAIN);
|
||||
h.update([point.tag()]);
|
||||
h.update(ALG_ID);
|
||||
for part in [input_bytes, signature_bytes, verifier_spki_der] {
|
||||
let part_hash = sha2::Sha256::digest(part);
|
||||
h.update(part_hash);
|
||||
}
|
||||
h.finalize().into()
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct CacheValue {
|
||||
pub verified_at_unix_secs: i64,
|
||||
pub crypto_impl_version: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct CryptoSigVerifyPointStats {
|
||||
pub calls: u64,
|
||||
/// Key already present in the cache (positive conclusion available).
|
||||
pub would_hit: u64,
|
||||
/// Key not present; after a successful verification it is inserted.
|
||||
pub new_keys: u64,
|
||||
/// Real cryptographic verification executed (always equals `calls` in observe-only
|
||||
/// mode; equals `new_keys` plus hit-but-observe calls otherwise).
|
||||
pub verify_executed: u64,
|
||||
/// Real verification skipped because of a cache hit (only > 0 when reuse is enabled).
|
||||
pub verify_skipped: u64,
|
||||
pub verify_nanos_on_would_hit: u64,
|
||||
pub verify_nanos_on_new_key: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct CryptoSigCacheSummary {
|
||||
pub schema_version: u32,
|
||||
pub cache_file: String,
|
||||
pub reuse_enabled: bool,
|
||||
pub capacity: u64,
|
||||
pub entries_loaded: u64,
|
||||
pub entries_total: u64,
|
||||
pub evictions_total: u64,
|
||||
pub per_point: BTreeMap<String, CryptoSigVerifyPointStats>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PointCounters {
|
||||
calls: AtomicU64,
|
||||
would_hit: AtomicU64,
|
||||
new_keys: AtomicU64,
|
||||
verify_executed: AtomicU64,
|
||||
verify_skipped: AtomicU64,
|
||||
verify_nanos_on_would_hit: AtomicU64,
|
||||
verify_nanos_on_new_key: AtomicU64,
|
||||
}
|
||||
|
||||
fn shard_index(key: &[u8; 32]) -> usize {
|
||||
(key[0] as usize) % SHARD_COUNT
|
||||
}
|
||||
|
||||
/// The persistent positive-conclusion signature cache plus per-run statistics.
|
||||
///
|
||||
/// See the module docs for storage format, eviction, and failure semantics.
|
||||
pub struct CryptoSigCache {
|
||||
cache_file: PathBuf,
|
||||
reuse_enabled: bool,
|
||||
capacity: usize,
|
||||
entries_loaded: u64,
|
||||
shards: Vec<Mutex<HashMap<[u8; 32], CacheValue>>>,
|
||||
len: AtomicUsize,
|
||||
evictions: AtomicU64,
|
||||
eviction_lock: Mutex<()>,
|
||||
points: [PointCounters; 5],
|
||||
}
|
||||
|
||||
impl CryptoSigCache {
|
||||
/// Load the cache from `cache_file`; start empty when the file is missing, corrupt, or
|
||||
/// has an unknown schema header. `reuse_enabled` selects the M2 hit-skips-verify
|
||||
/// behavior; when false the cache is observe-only (M1 semantics).
|
||||
pub fn load_or_rebuild(cache_file: PathBuf, reuse_enabled: bool) -> Self {
|
||||
Self::load_or_rebuild_with_capacity(cache_file, reuse_enabled, DEFAULT_CAPACITY)
|
||||
}
|
||||
|
||||
pub fn load_or_rebuild_with_capacity(
|
||||
cache_file: PathBuf,
|
||||
reuse_enabled: bool,
|
||||
capacity: usize,
|
||||
) -> Self {
|
||||
let entries = load_cache_file(&cache_file).unwrap_or_default();
|
||||
let mut shards = Vec::with_capacity(SHARD_COUNT);
|
||||
for _ in 0..SHARD_COUNT {
|
||||
shards.push(Mutex::new(HashMap::new()));
|
||||
}
|
||||
let mut len = 0usize;
|
||||
for (key, value) in entries {
|
||||
// Respect the cap even when loading a foreign/oversized file: keep the first
|
||||
// `capacity` records in file order.
|
||||
if len >= capacity {
|
||||
break;
|
||||
}
|
||||
shards[shard_index(&key)]
|
||||
.lock()
|
||||
.expect("crypto sig shard lock")
|
||||
.insert(key, value);
|
||||
len += 1;
|
||||
}
|
||||
Self {
|
||||
cache_file,
|
||||
reuse_enabled,
|
||||
capacity,
|
||||
entries_loaded: len as u64,
|
||||
shards,
|
||||
len: AtomicUsize::new(len),
|
||||
evictions: AtomicU64::new(0),
|
||||
eviction_lock: Mutex::new(()),
|
||||
points: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reuse_enabled(&self) -> bool {
|
||||
self.reuse_enabled
|
||||
}
|
||||
|
||||
fn lookup(&self, key: &[u8; 32]) -> bool {
|
||||
self.shards[shard_index(key)]
|
||||
.lock()
|
||||
.expect("crypto sig shard lock")
|
||||
.contains_key(key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn contains(&self, key: &[u8; 32]) -> bool {
|
||||
self.lookup(key)
|
||||
}
|
||||
|
||||
/// Insert a positive conclusion (used by the verify path and by tests).
|
||||
pub fn insert_positive(&self, key: [u8; 32]) {
|
||||
let value = CacheValue {
|
||||
verified_at_unix_secs: time::OffsetDateTime::now_utc().unix_timestamp(),
|
||||
crypto_impl_version: CRYPTO_IMPL_VERSION,
|
||||
};
|
||||
if self.len.load(Ordering::Relaxed) >= self.capacity {
|
||||
self.evict_all();
|
||||
}
|
||||
let mut shard = self.shards[shard_index(&key)]
|
||||
.lock()
|
||||
.expect("crypto sig shard lock");
|
||||
if shard.insert(key, value).is_none() {
|
||||
self.len.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
/// Full-rebuild eviction: clear every shard. See the module docs for the semantics.
|
||||
fn evict_all(&self) {
|
||||
let _guard = self.eviction_lock.lock().expect("crypto sig eviction lock");
|
||||
if self.len.load(Ordering::Relaxed) < self.capacity {
|
||||
// Another thread already evicted.
|
||||
return;
|
||||
}
|
||||
for shard in &self.shards {
|
||||
shard.lock().expect("crypto sig shard lock").clear();
|
||||
}
|
||||
self.len.store(0, Ordering::Relaxed);
|
||||
self.evictions.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Cache-aware verification: observe-only or reuse depending on `reuse_enabled`.
|
||||
/// The `verify` closure must perform exactly the real cryptographic verification.
|
||||
pub fn verify<E>(
|
||||
&self,
|
||||
point: CryptoSigVerifyPoint,
|
||||
input_bytes: &[u8],
|
||||
signature_bytes: &[u8],
|
||||
verifier_spki_der: &[u8],
|
||||
verify: impl FnOnce() -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
let key = compute_cache_key(point, input_bytes, signature_bytes, verifier_spki_der);
|
||||
let hit = self.lookup(&key);
|
||||
let counters = &self.points[(point.tag() - 1) as usize];
|
||||
counters.calls.fetch_add(1, Ordering::Relaxed);
|
||||
if hit {
|
||||
counters.would_hit.fetch_add(1, Ordering::Relaxed);
|
||||
if self.reuse_enabled {
|
||||
counters.verify_skipped.fetch_add(1, Ordering::Relaxed);
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
counters.new_keys.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
counters.verify_executed.fetch_add(1, Ordering::Relaxed);
|
||||
let started = std::time::Instant::now();
|
||||
let result = verify();
|
||||
let nanos = started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64;
|
||||
if hit {
|
||||
counters
|
||||
.verify_nanos_on_would_hit
|
||||
.fetch_add(nanos, Ordering::Relaxed);
|
||||
} else {
|
||||
counters
|
||||
.verify_nanos_on_new_key
|
||||
.fetch_add(nanos, Ordering::Relaxed);
|
||||
if result.is_ok() {
|
||||
self.insert_positive(key);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Persist the cache atomically (write-then-rename).
|
||||
pub fn persist(&self) -> Result<(), String> {
|
||||
persist_cache_file(&self.cache_file, &self.shards)
|
||||
}
|
||||
|
||||
pub fn summary(&self) -> CryptoSigCacheSummary {
|
||||
let mut per_point = BTreeMap::new();
|
||||
for (index, counters) in self.points.iter().enumerate() {
|
||||
let stats = CryptoSigVerifyPointStats {
|
||||
calls: counters.calls.load(Ordering::Relaxed),
|
||||
would_hit: counters.would_hit.load(Ordering::Relaxed),
|
||||
new_keys: counters.new_keys.load(Ordering::Relaxed),
|
||||
verify_executed: counters.verify_executed.load(Ordering::Relaxed),
|
||||
verify_skipped: counters.verify_skipped.load(Ordering::Relaxed),
|
||||
verify_nanos_on_would_hit: counters
|
||||
.verify_nanos_on_would_hit
|
||||
.load(Ordering::Relaxed),
|
||||
verify_nanos_on_new_key: counters
|
||||
.verify_nanos_on_new_key
|
||||
.load(Ordering::Relaxed),
|
||||
};
|
||||
if stats.calls > 0 {
|
||||
per_point.insert(
|
||||
CryptoSigVerifyPoint::from_index(index).name().to_string(),
|
||||
stats,
|
||||
);
|
||||
}
|
||||
}
|
||||
CryptoSigCacheSummary {
|
||||
schema_version: CACHE_SCHEMA_VERSION,
|
||||
cache_file: self.cache_file.to_string_lossy().into_owned(),
|
||||
reuse_enabled: self.reuse_enabled,
|
||||
capacity: self.capacity as u64,
|
||||
entries_loaded: self.entries_loaded,
|
||||
entries_total: self.len.load(Ordering::Relaxed) as u64,
|
||||
evictions_total: self.evictions.load(Ordering::Relaxed),
|
||||
per_point,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Default cache file location: a sibling of the work DB directory, mirroring
|
||||
/// `default_pp_cache_index_dir` (`<work-db-name>.crypto-sig-cache`).
|
||||
pub fn default_cache_file_path(db_path: &Path) -> PathBuf {
|
||||
let file_name = db_path
|
||||
.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.unwrap_or("work-db");
|
||||
db_path.with_file_name(format!("{file_name}.crypto-sig-cache"))
|
||||
}
|
||||
|
||||
fn load_cache_file(path: &Path) -> Option<HashMap<[u8; 32], CacheValue>> {
|
||||
let bytes = std::fs::read(path).ok()?;
|
||||
let header_len = CACHE_FILE_MAGIC.len() + 8;
|
||||
if bytes.len() < header_len || &bytes[..CACHE_FILE_MAGIC.len()] != CACHE_FILE_MAGIC {
|
||||
return None;
|
||||
}
|
||||
let mut count_bytes = [0u8; 8];
|
||||
count_bytes.copy_from_slice(&bytes[CACHE_FILE_MAGIC.len()..header_len]);
|
||||
let count = u64::from_le_bytes(count_bytes) as usize;
|
||||
let body = &bytes[header_len..];
|
||||
if body.len() != count.saturating_mul(RECORD_BYTES) {
|
||||
return None;
|
||||
}
|
||||
let mut entries = HashMap::with_capacity(count);
|
||||
for chunk in body.chunks_exact(RECORD_BYTES) {
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&chunk[..32]);
|
||||
let mut secs_bytes = [0u8; 8];
|
||||
secs_bytes.copy_from_slice(&chunk[32..40]);
|
||||
let mut version_bytes = [0u8; 4];
|
||||
version_bytes.copy_from_slice(&chunk[40..44]);
|
||||
entries.insert(
|
||||
key,
|
||||
CacheValue {
|
||||
verified_at_unix_secs: i64::from_le_bytes(secs_bytes),
|
||||
crypto_impl_version: u32::from_le_bytes(version_bytes),
|
||||
},
|
||||
);
|
||||
}
|
||||
Some(entries)
|
||||
}
|
||||
|
||||
fn persist_cache_file(
|
||||
path: &Path,
|
||||
shards: &[Mutex<HashMap<[u8; 32], CacheValue>>],
|
||||
) -> Result<(), String> {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(CACHE_FILE_MAGIC);
|
||||
// Placeholder for the record count; patched after collecting.
|
||||
bytes.extend_from_slice(&[0u8; 8]);
|
||||
let mut count = 0u64;
|
||||
for shard in shards {
|
||||
let map = shard.lock().expect("crypto sig shard lock");
|
||||
for (key, value) in map.iter() {
|
||||
bytes.extend_from_slice(key);
|
||||
bytes.extend_from_slice(&value.verified_at_unix_secs.to_le_bytes());
|
||||
bytes.extend_from_slice(&value.crypto_impl_version.to_le_bytes());
|
||||
bytes.extend_from_slice(&[0u8; 4]);
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
bytes[CACHE_FILE_MAGIC.len()..CACHE_FILE_MAGIC.len() + 8]
|
||||
.copy_from_slice(&count.to_le_bytes());
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.map_err(|e| format!("create cache-file dir failed: {}: {e}", parent.display()))?;
|
||||
}
|
||||
let tmp_path = path.with_extension("tmp");
|
||||
std::fs::write(&tmp_path, &bytes)
|
||||
.map_err(|e| format!("write cache file failed: {}: {e}", tmp_path.display()))?;
|
||||
std::fs::rename(&tmp_path, path).map_err(|e| {
|
||||
format!(
|
||||
"rename cache file failed: {} -> {}: {e}",
|
||||
tmp_path.display(),
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
static GLOBAL_CACHE: RwLock<Option<Arc<CryptoSigCache>>> = RwLock::new(None);
|
||||
|
||||
/// Install the process-global cache (called once from the CLI run path when
|
||||
/// `--crypto-signature-cache-observe-only` and/or `--enable-crypto-signature-cache` is set).
|
||||
pub fn install_global(cache: Arc<CryptoSigCache>) {
|
||||
*GLOBAL_CACHE.write().expect("crypto sig cache lock") = Some(cache);
|
||||
}
|
||||
|
||||
/// Remove the process-global cache. Used by tests and by the CLI after the run finished.
|
||||
pub fn clear_global() {
|
||||
*GLOBAL_CACHE.write().expect("crypto sig cache lock") = None;
|
||||
}
|
||||
|
||||
pub fn global_cache() -> Option<Arc<CryptoSigCache>> {
|
||||
GLOBAL_CACHE
|
||||
.read()
|
||||
.expect("crypto sig cache lock")
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Choke-point hook: cache-aware wrapper around a real signature verification.
|
||||
///
|
||||
/// With no cache installed this is a single read-lock check plus the verification itself;
|
||||
/// verification semantics never change either way (a hit only skips the crypto primitive,
|
||||
/// never the surrounding revocation/time/policy checks).
|
||||
pub fn verify_with_cache<E>(
|
||||
point: CryptoSigVerifyPoint,
|
||||
input_bytes: &[u8],
|
||||
signature_bytes: &[u8],
|
||||
verifier_spki_der: &[u8],
|
||||
verify: impl FnOnce() -> Result<(), E>,
|
||||
) -> Result<(), E> {
|
||||
let Some(cache) = global_cache() else {
|
||||
return verify();
|
||||
};
|
||||
cache.verify(
|
||||
point,
|
||||
input_bytes,
|
||||
signature_bytes,
|
||||
verifier_spki_der,
|
||||
verify,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) static GLOBAL_TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn temp_cache(reuse_enabled: bool) -> (tempfile::TempDir, CryptoSigCache) {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let cache = CryptoSigCache::load_or_rebuild(
|
||||
dir.path().join("work-db.crypto-sig-cache"),
|
||||
reuse_enabled,
|
||||
);
|
||||
(dir, cache)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_key_is_deterministic_and_domain_separated() {
|
||||
let a = compute_cache_key(
|
||||
CryptoSigVerifyPoint::CmsSignedObject,
|
||||
b"input",
|
||||
b"sig",
|
||||
b"spki",
|
||||
);
|
||||
let b = compute_cache_key(
|
||||
CryptoSigVerifyPoint::CmsSignedObject,
|
||||
b"input",
|
||||
b"sig",
|
||||
b"spki",
|
||||
);
|
||||
assert_eq!(a, b);
|
||||
|
||||
// Different verify point.
|
||||
assert_ne!(
|
||||
a,
|
||||
compute_cache_key(CryptoSigVerifyPoint::Crl, b"input", b"sig", b"spki")
|
||||
);
|
||||
// Different input bytes.
|
||||
assert_ne!(
|
||||
a,
|
||||
compute_cache_key(
|
||||
CryptoSigVerifyPoint::CmsSignedObject,
|
||||
b"input2",
|
||||
b"sig",
|
||||
b"spki"
|
||||
)
|
||||
);
|
||||
// Different signature bytes over the same input must not share a key.
|
||||
assert_ne!(
|
||||
a,
|
||||
compute_cache_key(
|
||||
CryptoSigVerifyPoint::CmsSignedObject,
|
||||
b"input",
|
||||
b"sig2",
|
||||
b"spki"
|
||||
)
|
||||
);
|
||||
// Different verifier key.
|
||||
assert_ne!(
|
||||
a,
|
||||
compute_cache_key(
|
||||
CryptoSigVerifyPoint::CmsSignedObject,
|
||||
b"input",
|
||||
b"sig",
|
||||
b"spki2"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miss_verifies_and_writes_then_hit_skips_verify() {
|
||||
let (_dir, cache) = temp_cache(true);
|
||||
let mut executed = 0u64;
|
||||
// First call: miss -> real verification runs and passes -> entry written.
|
||||
cache
|
||||
.verify(
|
||||
CryptoSigVerifyPoint::EeCertFast,
|
||||
b"tbs",
|
||||
b"sig",
|
||||
b"spki",
|
||||
|| -> Result<(), String> {
|
||||
executed += 1;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("verify ok");
|
||||
assert_eq!(executed, 1);
|
||||
// Second call: hit -> verification skipped entirely.
|
||||
cache
|
||||
.verify(
|
||||
CryptoSigVerifyPoint::EeCertFast,
|
||||
b"tbs",
|
||||
b"sig",
|
||||
b"spki",
|
||||
|| -> Result<(), String> {
|
||||
executed += 1;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("verify ok");
|
||||
assert_eq!(executed, 1, "cache hit must skip the real verification");
|
||||
let summary = cache.summary();
|
||||
let stats = summary.per_point.get("ee_cert_fast").expect("stats present");
|
||||
assert_eq!(stats.calls, 2);
|
||||
assert_eq!(stats.new_keys, 1);
|
||||
assert_eq!(stats.would_hit, 1);
|
||||
assert_eq!(stats.verify_executed, 1);
|
||||
assert_eq!(stats.verify_skipped, 1);
|
||||
assert!(summary.reuse_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preset_positive_key_skips_verify_that_would_fail() {
|
||||
let (_dir, cache) = temp_cache(true);
|
||||
// Tampered object bytes whose signature can never verify; pre-seed the positive
|
||||
// conclusion directly (as if a previous run had verified the untampered object).
|
||||
let key = compute_cache_key(
|
||||
CryptoSigVerifyPoint::CmsSignedObject,
|
||||
b"tampered-input",
|
||||
b"tampered-sig",
|
||||
b"spki",
|
||||
);
|
||||
cache.insert_positive(key);
|
||||
let mut executed = 0u64;
|
||||
let result = cache.verify(
|
||||
CryptoSigVerifyPoint::CmsSignedObject,
|
||||
b"tampered-input",
|
||||
b"tampered-sig",
|
||||
b"spki",
|
||||
|| -> Result<(), String> {
|
||||
executed += 1;
|
||||
Err("signature does not verify".to_string())
|
||||
},
|
||||
);
|
||||
assert_eq!(result, Ok(()), "cache hit must return Ok without verifying");
|
||||
assert_eq!(executed, 0, "real verification must not run on a hit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_verify_is_not_cached_and_reruns_next_time() {
|
||||
let (_dir, cache) = temp_cache(true);
|
||||
let mut executed = 0u64;
|
||||
for _ in 0..2 {
|
||||
let result = cache.verify(
|
||||
CryptoSigVerifyPoint::Crl,
|
||||
b"tbs",
|
||||
b"sig",
|
||||
b"spki",
|
||||
|| -> Result<(), String> {
|
||||
executed += 1;
|
||||
Err("bad signature".to_string())
|
||||
},
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
assert_eq!(executed, 2, "no negative caching: every call re-verifies");
|
||||
let key = compute_cache_key(CryptoSigVerifyPoint::Crl, b"tbs", b"sig", b"spki");
|
||||
assert!(!cache.contains(&key));
|
||||
let summary = cache.summary();
|
||||
let stats = summary.per_point.get("crl").expect("stats present");
|
||||
assert_eq!(stats.new_keys, 2);
|
||||
assert_eq!(stats.verify_executed, 2);
|
||||
assert_eq!(stats.verify_skipped, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn observe_only_mode_counts_would_hit_but_still_verifies() {
|
||||
let (_dir, cache) = temp_cache(false);
|
||||
let key = compute_cache_key(CryptoSigVerifyPoint::ChildCaCert, b"tbs", b"sig", b"spki");
|
||||
cache.insert_positive(key);
|
||||
let mut executed = 0u64;
|
||||
cache
|
||||
.verify(
|
||||
CryptoSigVerifyPoint::ChildCaCert,
|
||||
b"tbs",
|
||||
b"sig",
|
||||
b"spki",
|
||||
|| -> Result<(), String> {
|
||||
executed += 1;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("verify ok");
|
||||
assert_eq!(executed, 1, "observe-only mode must still run the verification");
|
||||
let summary = cache.summary();
|
||||
assert!(!summary.reuse_enabled);
|
||||
let stats = summary
|
||||
.per_point
|
||||
.get("child_ca_cert")
|
||||
.expect("stats present");
|
||||
assert_eq!(stats.would_hit, 1);
|
||||
assert_eq!(stats.verify_executed, 1);
|
||||
assert_eq!(stats.verify_skipped, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_file_roundtrips_entries_across_reload() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("work-db.crypto-sig-cache");
|
||||
let cache = CryptoSigCache::load_or_rebuild(path.clone(), true);
|
||||
let key = compute_cache_key(CryptoSigVerifyPoint::EeCertFast, b"tbs", b"sig", b"spki");
|
||||
cache.insert_positive(key);
|
||||
cache.persist().expect("persist");
|
||||
|
||||
let reloaded = CryptoSigCache::load_or_rebuild(path, true);
|
||||
assert!(reloaded.contains(&key));
|
||||
let summary = reloaded.summary();
|
||||
assert_eq!(summary.entries_loaded, 1);
|
||||
assert_eq!(summary.entries_total, 1);
|
||||
assert_eq!(summary.schema_version, CACHE_SCHEMA_VERSION);
|
||||
|
||||
// Reloaded entry drives a skip.
|
||||
let mut executed = 0u64;
|
||||
reloaded
|
||||
.verify(
|
||||
CryptoSigVerifyPoint::EeCertFast,
|
||||
b"tbs",
|
||||
b"sig",
|
||||
b"spki",
|
||||
|| -> Result<(), String> {
|
||||
executed += 1;
|
||||
Ok(())
|
||||
},
|
||||
)
|
||||
.expect("verify ok");
|
||||
assert_eq!(executed, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_file_rebuilds_on_missing_corrupt_or_mismatched_schema() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let path = dir.path().join("work-db.crypto-sig-cache");
|
||||
|
||||
// Missing file.
|
||||
let cache = CryptoSigCache::load_or_rebuild(path.clone(), true);
|
||||
assert_eq!(cache.summary().entries_loaded, 0);
|
||||
|
||||
// Garbage content.
|
||||
std::fs::write(&path, b"not a valid cache file").expect("write garbage");
|
||||
let cache = CryptoSigCache::load_or_rebuild(path.clone(), true);
|
||||
assert_eq!(cache.summary().entries_loaded, 0);
|
||||
|
||||
// M1 seen-set format (RPKICSC1) must not load as a v2 cache: cold start.
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(b"RPKICSC1");
|
||||
bytes.extend_from_slice(&1u64.to_le_bytes());
|
||||
bytes.extend_from_slice(&[7u8; 32]);
|
||||
std::fs::write(&path, &bytes).expect("write v1 file");
|
||||
let cache = CryptoSigCache::load_or_rebuild(path.clone(), true);
|
||||
assert_eq!(cache.summary().entries_loaded, 0);
|
||||
|
||||
// Valid v2 header but truncated body.
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(CACHE_FILE_MAGIC);
|
||||
bytes.extend_from_slice(&2u64.to_le_bytes());
|
||||
bytes.extend_from_slice(&[9u8; RECORD_BYTES]);
|
||||
std::fs::write(&path, &bytes).expect("write truncated");
|
||||
let cache = CryptoSigCache::load_or_rebuild(path, true);
|
||||
assert_eq!(cache.summary().entries_loaded, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn size_cap_triggers_full_rebuild_eviction() {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let cache = CryptoSigCache::load_or_rebuild_with_capacity(
|
||||
dir.path().join("work-db.crypto-sig-cache"),
|
||||
true,
|
||||
2,
|
||||
);
|
||||
let k1 = compute_cache_key(CryptoSigVerifyPoint::Crl, b"a", b"s", b"k");
|
||||
let k2 = compute_cache_key(CryptoSigVerifyPoint::Crl, b"b", b"s", b"k");
|
||||
let k3 = compute_cache_key(CryptoSigVerifyPoint::Crl, b"c", b"s", b"k");
|
||||
cache.insert_positive(k1);
|
||||
cache.insert_positive(k2);
|
||||
assert!(cache.contains(&k1) && cache.contains(&k2));
|
||||
// Third insert exceeds the cap -> full rebuild, then insert.
|
||||
cache.insert_positive(k3);
|
||||
assert!(!cache.contains(&k1));
|
||||
assert!(!cache.contains(&k2));
|
||||
assert!(cache.contains(&k3));
|
||||
let summary = cache.summary();
|
||||
assert_eq!(summary.entries_total, 1);
|
||||
assert_eq!(summary.evictions_total, 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_cache_file_path_mirrors_pp_cache_index_placement() {
|
||||
let db = Path::new("/state/db/work-db");
|
||||
assert_eq!(
|
||||
default_cache_file_path(db),
|
||||
PathBuf::from("/state/db/work-db.crypto-sig-cache")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_with_cache_without_global_cache_runs_verify_and_collects_nothing() {
|
||||
let _guard = GLOBAL_TEST_LOCK.lock().expect("test lock");
|
||||
clear_global();
|
||||
assert!(global_cache().is_none());
|
||||
let mut executed = 0u64;
|
||||
let result: Result<u64, String> = (|| {
|
||||
verify_with_cache(
|
||||
CryptoSigVerifyPoint::CmsSignedObject,
|
||||
b"input",
|
||||
b"sig",
|
||||
b"spki",
|
||||
|| -> Result<(), String> {
|
||||
executed += 1;
|
||||
Ok(())
|
||||
},
|
||||
)?;
|
||||
Ok(42)
|
||||
})();
|
||||
assert_eq!(result, Ok(42));
|
||||
assert_eq!(executed, 1);
|
||||
assert!(global_cache().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_with_cache_preserves_result_through_global_install() {
|
||||
let _guard = GLOBAL_TEST_LOCK.lock().expect("test lock");
|
||||
clear_global();
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let cache = Arc::new(CryptoSigCache::load_or_rebuild(
|
||||
dir.path().join("work-db.crypto-sig-cache"),
|
||||
true,
|
||||
));
|
||||
install_global(Arc::clone(&cache));
|
||||
|
||||
let ok: Result<(), String> = verify_with_cache(
|
||||
CryptoSigVerifyPoint::ChildCaCert,
|
||||
b"tbs",
|
||||
b"sig",
|
||||
b"spki",
|
||||
|| Ok(()),
|
||||
);
|
||||
assert_eq!(ok, Ok(()));
|
||||
let err: Result<(), String> = verify_with_cache(
|
||||
CryptoSigVerifyPoint::ChildCaCert,
|
||||
b"tbs2",
|
||||
b"sig",
|
||||
b"spki",
|
||||
|| Err("bad signature".to_string()),
|
||||
);
|
||||
assert_eq!(err, Err("bad signature".to_string()));
|
||||
let summary = cache.summary();
|
||||
let stats = summary
|
||||
.per_point
|
||||
.get("child_ca_cert")
|
||||
.expect("stats present");
|
||||
assert_eq!(stats.calls, 2);
|
||||
assert_eq!(stats.new_keys, 2);
|
||||
clear_global();
|
||||
assert!(global_cache().is_none());
|
||||
}
|
||||
}
|
||||
@ -285,8 +285,16 @@ impl RpkixCrl {
|
||||
if !rem.is_empty() {
|
||||
return Err(CrlVerifyError::CrlTrailingBytes(rem.len()));
|
||||
}
|
||||
crate::crypto_sig_cache::verify_with_cache(
|
||||
crate::crypto_sig_cache::CryptoSigVerifyPoint::Crl,
|
||||
crl.tbs_cert_list.as_ref(),
|
||||
crl.signature_value.data.as_ref(),
|
||||
issuer_spki.raw,
|
||||
|| {
|
||||
crl.verify_signature(issuer_spki)
|
||||
.map_err(|e| CrlVerifyError::InvalidSignature(e.to_string()))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Verify the cryptographic signature on this CRL using a DER-encoded SubjectPublicKeyInfo.
|
||||
|
||||
@ -358,7 +358,19 @@ impl RpkiSignedObject {
|
||||
/// Verify the CMS signature using the embedded EE certificate public key.
|
||||
pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> {
|
||||
let ee = &self.signed_data.certificates[0];
|
||||
self.verify_signature_with_rsa_components(&ee.rsa_public_modulus, &ee.rsa_public_exponent)
|
||||
let signer = &self.signed_data.signer_infos[0];
|
||||
crate::crypto_sig_cache::verify_with_cache(
|
||||
crate::crypto_sig_cache::CryptoSigVerifyPoint::CmsSignedObject,
|
||||
&signer.signed_attrs_der_for_signature,
|
||||
&signer.signature,
|
||||
&ee.spki_der,
|
||||
|| {
|
||||
self.verify_signature_with_rsa_components(
|
||||
&ee.rsa_public_modulus,
|
||||
&ee.rsa_public_exponent,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/// Verify the CMS signature using a DER-encoded SubjectPublicKeyInfo.
|
||||
@ -394,7 +406,14 @@ impl RpkiSignedObject {
|
||||
_ => return Err(SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm),
|
||||
};
|
||||
|
||||
self.verify_signature_with_rsa_components(n.as_slice(), e.as_slice())
|
||||
let signer = &self.signed_data.signer_infos[0];
|
||||
crate::crypto_sig_cache::verify_with_cache(
|
||||
crate::crypto_sig_cache::CryptoSigVerifyPoint::CmsSignedObject,
|
||||
&signer.signed_attrs_der_for_signature,
|
||||
&signer.signature,
|
||||
ee_spki.raw,
|
||||
|| self.verify_signature_with_rsa_components(n.as_slice(), e.as_slice()),
|
||||
)
|
||||
}
|
||||
|
||||
fn verify_signature_with_rsa_components(
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
pub mod ccr;
|
||||
pub mod cir;
|
||||
pub mod crypto_sig_cache;
|
||||
pub mod data_model;
|
||||
|
||||
#[cfg(feature = "full")]
|
||||
|
||||
@ -485,9 +485,17 @@ fn verify_child_signature(
|
||||
child: &X509Certificate<'_>,
|
||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||
) -> Result<(), CaPathError> {
|
||||
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> {
|
||||
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> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,11 +21,13 @@ pub struct ValidationCacheContract {
|
||||
pub roa: bool,
|
||||
pub child_certificate: bool,
|
||||
pub transport_prefetch: bool,
|
||||
#[serde(default)]
|
||||
pub crypto_signature: bool,
|
||||
}
|
||||
|
||||
impl ValidationCacheContract {
|
||||
pub fn any_validation_cache_enabled(&self) -> bool {
|
||||
self.publication_point || self.roa || self.child_certificate
|
||||
self.publication_point || self.roa || self.child_certificate || self.crypto_signature
|
||||
}
|
||||
}
|
||||
|
||||
@ -358,7 +360,7 @@ pub fn prepare_verification(
|
||||
contract.require_current_binary()?;
|
||||
if !contract.cache.any_validation_cache_enabled() {
|
||||
return Err(
|
||||
"source run did not enable PP, ROA, or child-certificate validation cache".to_string(),
|
||||
"source run did not enable PP, ROA, child-certificate, or crypto signature validation cache".to_string(),
|
||||
);
|
||||
}
|
||||
if !artifacts.source_work_db.is_dir() {
|
||||
@ -645,6 +647,7 @@ mod tests {
|
||||
roa: true,
|
||||
child_certificate: true,
|
||||
transport_prefetch: false,
|
||||
crypto_signature: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -751,6 +754,7 @@ mod tests {
|
||||
roa: false,
|
||||
child_certificate: false,
|
||||
transport_prefetch: true,
|
||||
crypto_signature: false,
|
||||
},
|
||||
)
|
||||
.expect("contract");
|
||||
@ -802,6 +806,7 @@ mod tests {
|
||||
roa: false,
|
||||
child_certificate: false,
|
||||
transport_prefetch: true,
|
||||
crypto_signature: false,
|
||||
};
|
||||
assert!(!cache.any_validation_cache_enabled());
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user