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_RUN_ROOT shared run root/state root; default PACKAGE_ROOT
|
||||||
EXPERIMENT_DIR experiment metadata output directory
|
EXPERIMENT_DIR experiment metadata output directory
|
||||||
CASE_RUNS delta runs per case; default 10
|
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
|
RUN_START_INTERVAL_SECS fixed start cadence for all runs; default 600
|
||||||
FIRST_RUN_DELAY_SECS delay before the first scheduled run; default 0
|
FIRST_RUN_DELAY_SECS delay before the first scheduled run; default 0
|
||||||
SNAPSHOT_EXTRA_ARGS extra rpki args for snapshot warmup
|
SNAPSHOT_EXTRA_ARGS extra rpki args for snapshot warmup
|
||||||
@ -95,7 +95,10 @@ case_count() {
|
|||||||
case "$EXPERIMENT_CASE_SET" in
|
case "$EXPERIMENT_CASE_SET" in
|
||||||
default) printf '%s' 4 ;;
|
default) printf '%s' 4 ;;
|
||||||
cache-only) printf '%s' 3 ;;
|
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
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,6 +111,14 @@ case_id_for_index() {
|
|||||||
cache-only:1) printf '%s' "pp-only" ;;
|
cache-only:1) printf '%s' "pp-only" ;;
|
||||||
cache-only:2) printf '%s' "object-only" ;;
|
cache-only:2) printf '%s' "object-only" ;;
|
||||||
cache-only:3) printf '%s' "pp-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" ;;
|
*) die "unknown case index: $1 for set $EXPERIMENT_CASE_SET" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@ -121,6 +132,14 @@ case_name_for_index() {
|
|||||||
cache-only:1) printf '%s' "pp-cache-only" ;;
|
cache-only:1) printf '%s' "pp-cache-only" ;;
|
||||||
cache-only:2) printf '%s' "object-cache-only" ;;
|
cache-only:2) printf '%s' "object-cache-only" ;;
|
||||||
cache-only:3) printf '%s' "pp-cache-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" ;;
|
*) die "unknown case index: $1 for set $EXPERIMENT_CASE_SET" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@ -134,14 +153,22 @@ case_extra_args_for_index() {
|
|||||||
cache-only:1) printf '%s' "--enable-publication-point-validation-cache" ;;
|
cache-only:1) printf '%s' "--enable-publication-point-validation-cache" ;;
|
||||||
cache-only:2) printf '%s' "--enable-roa-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" ;;
|
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" ;;
|
*) die "unknown case index: $1 for set $EXPERIMENT_CASE_SET" ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
case_child_cert_cache_for_index() {
|
case_child_cert_cache_for_index() {
|
||||||
case "$EXPERIMENT_CASE_SET:$1" in
|
case "$EXPERIMENT_CASE_SET:$1" in
|
||||||
default:4|cache-only:2|cache-only:3) printf '%s' "1" ;;
|
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) printf '%s' "0" ;;
|
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" ;;
|
*) die "unknown case index: $1 for set $EXPERIMENT_CASE_SET" ;;
|
||||||
esac
|
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": "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},
|
{"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:
|
else:
|
||||||
raise SystemExit(f"unknown case set: {case_set}")
|
raise SystemExit(f"unknown case set: {case_set}")
|
||||||
print(json.dumps(cases, ensure_ascii=False, indent=8))
|
print(json.dumps(cases, ensure_ascii=False, indent=8))
|
||||||
@ -345,6 +389,7 @@ else:
|
|||||||
"publication_point_cache_index_load",
|
"publication_point_cache_index_load",
|
||||||
"publication_point_cache_index_refresh",
|
"publication_point_cache_index_refresh",
|
||||||
"roa_validation_cache",
|
"roa_validation_cache",
|
||||||
|
"crypto_signature_cache_observe",
|
||||||
]:
|
]:
|
||||||
if key in stage:
|
if key in stage:
|
||||||
record[key] = stage.get(key)
|
record[key] = stage.get(key)
|
||||||
@ -360,6 +405,11 @@ else:
|
|||||||
"child_certificate_cache_miss_not_found",
|
"child_certificate_cache_miss_not_found",
|
||||||
"fresh_publication_points",
|
"fresh_publication_points",
|
||||||
"fresh_manifest_files_total",
|
"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["cacheCounts"] = {k: analysis_counts.get(k) for k in interesting if k in analysis_counts}
|
||||||
record["repoSyncStats"] = summary.get("repoSyncStats")
|
record["repoSyncStats"] = summary.get("repoSyncStats")
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
- `vrps.csv`、`vaps.csv`
|
- `vrps.csv`、`vaps.csv`
|
||||||
- `stage-timing.json`、`validation-events.jsonl`
|
- `stage-timing.json`、`validation-events.jsonl`
|
||||||
- `validation-contract.json`、`verification-meta.json`
|
- `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`
|
- `stdout.log`、`stderr.log`、`process-time.txt`、`wrapper-timing.json`
|
||||||
|
|
||||||
通过条件是 compare JSON 中 `stateDigestMatch=true`。`producedAt` 不参与比较。
|
通过条件是 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,
|
enable_child_certificate_validation_cache: bool,
|
||||||
publication_point_cache_observe_only: bool,
|
publication_point_cache_observe_only: bool,
|
||||||
enable_publication_point_validation_cache: bool,
|
enable_publication_point_validation_cache: bool,
|
||||||
|
crypto_signature_cache_observe: Option<crate::crypto_sig_cache::CryptoSigCacheSummary>,
|
||||||
enable_transport_request_prefetch: bool,
|
enable_transport_request_prefetch: bool,
|
||||||
report_build_ms: u64,
|
report_build_ms: u64,
|
||||||
report_write_ms: Option<u64>,
|
report_write_ms: Option<u64>,
|
||||||
@ -145,6 +146,8 @@ pub struct CliArgs {
|
|||||||
pub enable_child_certificate_validation_cache: bool,
|
pub enable_child_certificate_validation_cache: bool,
|
||||||
pub publication_point_cache_observe_only: bool,
|
pub publication_point_cache_observe_only: bool,
|
||||||
pub enable_publication_point_validation_cache: 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 enable_transport_request_prefetch: bool,
|
||||||
pub ccr_out_path: Option<PathBuf>,
|
pub ccr_out_path: Option<PathBuf>,
|
||||||
pub vrps_csv_out_path: Option<PathBuf>,
|
pub vrps_csv_out_path: Option<PathBuf>,
|
||||||
@ -222,6 +225,12 @@ Options:
|
|||||||
Evaluate publication-point cache eligibility without changing results
|
Evaluate publication-point cache eligibility without changing results
|
||||||
--enable-publication-point-validation-cache
|
--enable-publication-point-validation-cache
|
||||||
Experimental: reuse complete publication-point validation projections
|
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
|
--enable-transport-request-prefetch
|
||||||
Experimental: prefetch previous run transport repo requests before tree traversal
|
Experimental: prefetch previous run transport repo requests before tree traversal
|
||||||
--ccr-out <path> Write CCR DER ContentInfo to this path (optional)
|
--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_roa_validation_cache: bool = false;
|
||||||
let mut enable_child_certificate_validation_cache: bool = false;
|
let mut enable_child_certificate_validation_cache: bool = false;
|
||||||
let mut publication_point_cache_observe_only: 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_publication_point_validation_cache: bool = false;
|
||||||
let mut enable_transport_request_prefetch: bool = false;
|
let mut enable_transport_request_prefetch: bool = false;
|
||||||
let mut ccr_out_path: Option<PathBuf> = None;
|
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" => {
|
||||||
enable_publication_point_validation_cache = true;
|
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" => {
|
||||||
enable_transport_request_prefetch = true;
|
enable_transport_request_prefetch = true;
|
||||||
}
|
}
|
||||||
@ -808,6 +825,8 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
|||||||
|| enable_child_certificate_validation_cache
|
|| enable_child_certificate_validation_cache
|
||||||
|| publication_point_cache_observe_only
|
|| publication_point_cache_observe_only
|
||||||
|| enable_publication_point_validation_cache
|
|| enable_publication_point_validation_cache
|
||||||
|
|| crypto_signature_cache_observe_only
|
||||||
|
|| enable_crypto_signature_cache
|
||||||
|| enable_transport_request_prefetch
|
|| enable_transport_request_prefetch
|
||||||
|| rsync_local_dir.is_some()
|
|| rsync_local_dir.is_some()
|
||||||
|| rsync_command.is_some()
|
|| rsync_command.is_some()
|
||||||
@ -1121,6 +1140,8 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
|||||||
enable_child_certificate_validation_cache,
|
enable_child_certificate_validation_cache,
|
||||||
publication_point_cache_observe_only,
|
publication_point_cache_observe_only,
|
||||||
enable_publication_point_validation_cache,
|
enable_publication_point_validation_cache,
|
||||||
|
crypto_signature_cache_observe_only,
|
||||||
|
enable_crypto_signature_cache,
|
||||||
enable_transport_request_prefetch,
|
enable_transport_request_prefetch,
|
||||||
ccr_out_path,
|
ccr_out_path,
|
||||||
vrps_csv_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.enable_child_certificate_validation_cache = false;
|
||||||
args.publication_point_cache_observe_only = false;
|
args.publication_point_cache_observe_only = false;
|
||||||
args.enable_publication_point_validation_cache = 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.enable_transport_request_prefetch = false;
|
||||||
args.ccr_out_path = Some(prepared.artifacts.result_ccr.clone());
|
args.ccr_out_path = Some(prepared.artifacts.result_ccr.clone());
|
||||||
args.vrps_csv_out_path = Some(prepared.artifacts.vrps_csv.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,
|
roa: args.enable_roa_validation_cache,
|
||||||
child_certificate: args.enable_child_certificate_validation_cache,
|
child_certificate: args.enable_child_certificate_validation_cache,
|
||||||
transport_prefetch: args.enable_transport_request_prefetch,
|
transport_prefetch: args.enable_transport_request_prefetch,
|
||||||
|
crypto_signature: args.enable_crypto_signature_cache,
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
crate::verification_only::write_validation_contract(path, &contract)?;
|
crate::verification_only::write_validation_contract(path, &contract)?;
|
||||||
@ -2351,6 +2375,19 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
|||||||
store.as_ref(),
|
store.as_ref(),
|
||||||
);
|
);
|
||||||
let validation_started = std::time::Instant::now();
|
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 collect_current_repo_objects = false;
|
||||||
let out = if args.verification_only {
|
let out = if args.verification_only {
|
||||||
let http = crate::verification_only::NetworkDisabledHttpFetcher;
|
let http = crate::verification_only::NetworkDisabledHttpFetcher;
|
||||||
@ -2778,6 +2815,36 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
let publication_point_cache_index_load = store.publication_point_cache_mmap_index_load_stats();
|
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
|
let timing_report_snapshot = timing
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|(_, handle)| handle.report_snapshot(50));
|
.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,
|
enable_child_certificate_validation_cache: args.enable_child_certificate_validation_cache,
|
||||||
publication_point_cache_observe_only: args.publication_point_cache_observe_only,
|
publication_point_cache_observe_only: args.publication_point_cache_observe_only,
|
||||||
enable_publication_point_validation_cache: args.enable_publication_point_validation_cache,
|
enable_publication_point_validation_cache: args.enable_publication_point_validation_cache,
|
||||||
|
crypto_signature_cache_observe,
|
||||||
enable_transport_request_prefetch: args.enable_transport_request_prefetch,
|
enable_transport_request_prefetch: args.enable_transport_request_prefetch,
|
||||||
report_build_ms,
|
report_build_ms,
|
||||||
report_write_ms,
|
report_write_ms,
|
||||||
|
|||||||
@ -289,6 +289,56 @@ fn parse_accepts_publication_point_cache_flags() {
|
|||||||
assert!(args.enable_publication_point_validation_cache);
|
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]
|
#[test]
|
||||||
fn parse_accepts_enable_transport_request_prefetch() {
|
fn parse_accepts_enable_transport_request_prefetch() {
|
||||||
let argv = vec![
|
let argv = vec![
|
||||||
@ -1807,6 +1857,7 @@ fn run_report_task_and_stage_timing_work() {
|
|||||||
enable_child_certificate_validation_cache: false,
|
enable_child_certificate_validation_cache: false,
|
||||||
publication_point_cache_observe_only: false,
|
publication_point_cache_observe_only: false,
|
||||||
enable_publication_point_validation_cache: false,
|
enable_publication_point_validation_cache: false,
|
||||||
|
crypto_signature_cache_observe: None,
|
||||||
enable_transport_request_prefetch: false,
|
enable_transport_request_prefetch: false,
|
||||||
report_build_ms: report_output.report_build_ms,
|
report_build_ms: report_output.report_build_ms,
|
||||||
report_write_ms: report_output.report_write_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,
|
enable_child_certificate_validation_cache: true,
|
||||||
publication_point_cache_observe_only: false,
|
publication_point_cache_observe_only: false,
|
||||||
enable_publication_point_validation_cache: false,
|
enable_publication_point_validation_cache: false,
|
||||||
|
crypto_signature_cache_observe: None,
|
||||||
enable_transport_request_prefetch: true,
|
enable_transport_request_prefetch: true,
|
||||||
report_build_ms: 2,
|
report_build_ms: 2,
|
||||||
report_write_ms: None,
|
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() {
|
if !rem.is_empty() {
|
||||||
return Err(CrlVerifyError::CrlTrailingBytes(rem.len()));
|
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)
|
crl.verify_signature(issuer_spki)
|
||||||
.map_err(|e| CrlVerifyError::InvalidSignature(e.to_string()))
|
.map_err(|e| CrlVerifyError::InvalidSignature(e.to_string()))
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify the cryptographic signature on this CRL using a DER-encoded SubjectPublicKeyInfo.
|
/// 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.
|
/// Verify the CMS signature using the embedded EE certificate public key.
|
||||||
pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> {
|
pub fn verify_signature(&self) -> Result<(), SignedObjectVerifyError> {
|
||||||
let ee = &self.signed_data.certificates[0];
|
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.
|
/// Verify the CMS signature using a DER-encoded SubjectPublicKeyInfo.
|
||||||
@ -394,7 +406,14 @@ impl RpkiSignedObject {
|
|||||||
_ => return Err(SignedObjectVerifyError::UnsupportedEePublicKeyAlgorithm),
|
_ => 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(
|
fn verify_signature_with_rsa_components(
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
pub mod ccr;
|
pub mod ccr;
|
||||||
pub mod cir;
|
pub mod cir;
|
||||||
|
pub mod crypto_sig_cache;
|
||||||
pub mod data_model;
|
pub mod data_model;
|
||||||
|
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
|
|||||||
@ -485,9 +485,17 @@ fn verify_child_signature(
|
|||||||
child: &X509Certificate<'_>,
|
child: &X509Certificate<'_>,
|
||||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||||
) -> Result<(), CaPathError> {
|
) -> 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
|
child
|
||||||
.verify_signature(Some(issuer_spki))
|
.verify_signature(Some(issuer_spki))
|
||||||
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
.map_err(|e| CaPathError::ChildSignatureInvalid(e.to_string()))
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_child_aki_matches_issuer_ski(
|
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]
|
#[test]
|
||||||
fn issuer_effective_resources_index_and_indexed_resolvers_cover_success_and_failure_paths() {
|
fn issuer_effective_resources_index_and_indexed_resolvers_cover_success_and_failure_paths() {
|
||||||
use crate::data_model::rc::{AsIdOrRange, IpPrefix};
|
use crate::data_model::rc::{AsIdOrRange, IpPrefix};
|
||||||
|
|||||||
@ -385,8 +385,16 @@ fn verify_ee_cert_signature(
|
|||||||
ee: &X509Certificate<'_>,
|
ee: &X509Certificate<'_>,
|
||||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||||
) -> Result<(), CertPathError> {
|
) -> 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))
|
ee.verify_signature(Some(issuer_spki))
|
||||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_ee_cert_signature_fast(
|
fn verify_ee_cert_signature_fast(
|
||||||
@ -394,12 +402,20 @@ fn verify_ee_cert_signature_fast(
|
|||||||
ee_signature_bytes: &[u8],
|
ee_signature_bytes: &[u8],
|
||||||
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
issuer_spki: &SubjectPublicKeyInfo<'_>,
|
||||||
) -> Result<(), CertPathError> {
|
) -> 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(
|
let key = signature::UnparsedPublicKey::new(
|
||||||
&signature::RSA_PKCS1_2048_8192_SHA256,
|
&signature::RSA_PKCS1_2048_8192_SHA256,
|
||||||
&issuer_spki.subject_public_key.data,
|
&issuer_spki.subject_public_key.data,
|
||||||
);
|
);
|
||||||
key.verify(ee_tbs_der, ee_signature_bytes)
|
key.verify(ee_tbs_der, ee_signature_bytes)
|
||||||
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
.map_err(|e| CertPathError::EeSignatureInvalid(e.to_string()))
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_ee_aki_matches_issuer_ski(
|
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")
|
validate_ee_crldp_contains_issuer_crl_uri(&ee, "rsync://example.test/issuer.crl")
|
||||||
.expect("crldp ok");
|
.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 roa: bool,
|
||||||
pub child_certificate: bool,
|
pub child_certificate: bool,
|
||||||
pub transport_prefetch: bool,
|
pub transport_prefetch: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub crypto_signature: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValidationCacheContract {
|
impl ValidationCacheContract {
|
||||||
pub fn any_validation_cache_enabled(&self) -> bool {
|
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()?;
|
contract.require_current_binary()?;
|
||||||
if !contract.cache.any_validation_cache_enabled() {
|
if !contract.cache.any_validation_cache_enabled() {
|
||||||
return Err(
|
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() {
|
if !artifacts.source_work_db.is_dir() {
|
||||||
@ -645,6 +647,7 @@ mod tests {
|
|||||||
roa: true,
|
roa: true,
|
||||||
child_certificate: true,
|
child_certificate: true,
|
||||||
transport_prefetch: false,
|
transport_prefetch: false,
|
||||||
|
crypto_signature: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -751,6 +754,7 @@ mod tests {
|
|||||||
roa: false,
|
roa: false,
|
||||||
child_certificate: false,
|
child_certificate: false,
|
||||||
transport_prefetch: true,
|
transport_prefetch: true,
|
||||||
|
crypto_signature: false,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.expect("contract");
|
.expect("contract");
|
||||||
@ -802,6 +806,7 @@ mod tests {
|
|||||||
roa: false,
|
roa: false,
|
||||||
child_certificate: false,
|
child_certificate: false,
|
||||||
transport_prefetch: true,
|
transport_prefetch: true,
|
||||||
|
crypto_signature: false,
|
||||||
};
|
};
|
||||||
assert!(!cache.any_validation_cache_enabled());
|
assert!(!cache.any_validation_cache_enabled());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user