20260420_2 完成输出report和ccr优化,snapshot耗时优化到70多秒
This commit is contained in:
parent
f6a601e16c
commit
542bd7be80
@ -10,6 +10,7 @@ Usage:
|
||||
[--ssh-target <user@host>] \
|
||||
[--rpki-client-bin <path>] \
|
||||
[--libtls-path <path>] \
|
||||
[--rp-run-mode <serial|parallel>] \
|
||||
[--ours-extra-args '<args>'] \
|
||||
[--dry-run]
|
||||
EOF
|
||||
@ -21,6 +22,7 @@ REMOTE_ROOT=""
|
||||
SSH_TARGET="${SSH_TARGET:-root@47.251.56.108}"
|
||||
RPKI_CLIENT_BIN="${RPKI_CLIENT_BIN:-/home/yuyr/dev/rpki-client-9.7/build-m5/src/rpki-client}"
|
||||
LIBTLS_PATH="${LIBTLS_PATH:-/home/yuyr/dev/rpki-client-9.7/.deps/libtls/root/usr/lib/x86_64-linux-gnu/libtls.so.28.0.0}"
|
||||
RP_RUN_MODE="${RP_RUN_MODE:-serial}"
|
||||
OURS_EXTRA_ARGS="${OURS_EXTRA_ARGS:-}"
|
||||
DRY_RUN=0
|
||||
|
||||
@ -31,6 +33,7 @@ while [[ $# -gt 0 ]]; do
|
||||
--ssh-target) SSH_TARGET="$2"; shift 2 ;;
|
||||
--rpki-client-bin) RPKI_CLIENT_BIN="$2"; shift 2 ;;
|
||||
--libtls-path) LIBTLS_PATH="$2"; shift 2 ;;
|
||||
--rp-run-mode) RP_RUN_MODE="$2"; shift 2 ;;
|
||||
--ours-extra-args) OURS_EXTRA_ARGS="$2"; shift 2 ;;
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
@ -39,6 +42,7 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
[[ -n "$RUN_ROOT" && -n "$REMOTE_ROOT" ]] || { usage >&2; exit 2; }
|
||||
[[ "$RP_RUN_MODE" == "serial" || "$RP_RUN_MODE" == "parallel" ]] || { echo "invalid --rp-run-mode: $RP_RUN_MODE" >&2; usage; exit 2; }
|
||||
[[ "$DRY_RUN" -eq 1 || -x "$RPKI_CLIENT_BIN" ]] || { echo "rpki-client binary not executable: $RPKI_CLIENT_BIN" >&2; exit 2; }
|
||||
[[ "$DRY_RUN" -eq 1 || -f "$LIBTLS_PATH" ]] || { echo "libtls not found: $LIBTLS_PATH" >&2; exit 2; }
|
||||
|
||||
@ -64,6 +68,7 @@ scope=APNIC+ARIN mixed release two-step synchronized compare
|
||||
run_root=$RUN_ROOT
|
||||
remote_root=$REMOTE_ROOT
|
||||
ssh_target=$SSH_TARGET
|
||||
rp_run_mode=$RP_RUN_MODE
|
||||
ours_extra_args=$OURS_EXTRA_ARGS
|
||||
EOF2
|
||||
exit 0
|
||||
@ -76,12 +81,10 @@ cleanup_remote() {
|
||||
}
|
||||
trap cleanup_remote EXIT
|
||||
|
||||
if [[ ! -x "$ROOT_DIR/target/release/rpki" || ! -x "$ROOT_DIR/target/release/ccr_to_compare_views" ]]; then
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
cargo build --release --bin rpki --bin ccr_to_compare_views
|
||||
)
|
||||
fi
|
||||
(
|
||||
cd "$ROOT_DIR"
|
||||
cargo build --release --bin rpki --bin ccr_to_compare_views
|
||||
)
|
||||
|
||||
ssh "$SSH_TARGET" "set -e; id -u _rpki-client >/dev/null 2>&1 || useradd -r -M -s /usr/sbin/nologin _rpki-client || true; rm -rf '$REMOTE_ROOT'; mkdir -p '$REMOTE_ROOT/bin' '$REMOTE_ROOT/lib' '$REMOTE_ROOT/state/ours' '$REMOTE_ROOT/state/rpki-client' '$REMOTE_ROOT/steps/step-001/ours' '$REMOTE_ROOT/steps/step-001/rpki-client' '$REMOTE_ROOT/steps/step-002/ours' '$REMOTE_ROOT/steps/step-002/rpki-client'"
|
||||
scp "$ROOT_DIR/target/release/rpki" "$APNIC_TAL" "$APNIC_TA" "$ARIN_TAL" "$ARIN_TA" "$SSH_TARGET:$REMOTE_ROOT/"
|
||||
@ -93,12 +96,13 @@ run_step() {
|
||||
local kind="$2"
|
||||
local local_step="$RUN_ROOT/steps/$step_id"
|
||||
|
||||
ssh "$SSH_TARGET" bash -s -- "$REMOTE_ROOT" "$step_id" "$kind" "$OURS_EXTRA_ARGS" <<'EOS'
|
||||
ssh "$SSH_TARGET" bash -s -- "$REMOTE_ROOT" "$step_id" "$kind" "$OURS_EXTRA_ARGS" "$RP_RUN_MODE" <<'EOS'
|
||||
set -euo pipefail
|
||||
REMOTE_ROOT="$1"
|
||||
STEP_ID="$2"
|
||||
KIND="$3"
|
||||
OURS_EXTRA_ARGS="$4"
|
||||
RP_RUN_MODE="$5"
|
||||
|
||||
cd "$REMOTE_ROOT"
|
||||
mkdir -p "steps/$STEP_ID/ours" "steps/$STEP_ID/rpki-client"
|
||||
@ -121,7 +125,7 @@ print(time.time() + 3.0)
|
||||
PY
|
||||
)"
|
||||
|
||||
(
|
||||
run_ours() {
|
||||
python3 - <<'PY' "$START_EPOCH"
|
||||
import sys, time
|
||||
x = float(sys.argv[1])
|
||||
@ -166,10 +170,9 @@ json.dump(
|
||||
indent=2,
|
||||
)
|
||||
PY
|
||||
) &
|
||||
OURS_PID=$!
|
||||
}
|
||||
|
||||
(
|
||||
run_client() {
|
||||
cd state/rpki-client
|
||||
python3 - <<'PY' "$START_EPOCH"
|
||||
import sys, time
|
||||
@ -213,11 +216,19 @@ json.dump(
|
||||
indent=2,
|
||||
)
|
||||
PY
|
||||
) &
|
||||
CLIENT_PID=$!
|
||||
}
|
||||
|
||||
wait "$OURS_PID"
|
||||
wait "$CLIENT_PID"
|
||||
if [[ "$RP_RUN_MODE" == "parallel" ]]; then
|
||||
run_ours &
|
||||
OURS_PID=$!
|
||||
run_client &
|
||||
CLIENT_PID=$!
|
||||
wait "$OURS_PID"
|
||||
wait "$CLIENT_PID"
|
||||
else
|
||||
run_ours
|
||||
run_client
|
||||
fi
|
||||
EOS
|
||||
|
||||
for rel in result.ccr round-result.json run.log stage-timing.json; do
|
||||
@ -275,13 +286,14 @@ PY
|
||||
run_step step-001 snapshot
|
||||
run_step step-002 delta
|
||||
|
||||
python3 - <<'PY' "$RUN_ROOT/steps/step-001/step-summary.json" "$RUN_ROOT/steps/step-002/step-summary.json" "$RUN_ROOT/summary.json" "$OURS_EXTRA_ARGS"
|
||||
python3 - <<'PY' "$RUN_ROOT/steps/step-001/step-summary.json" "$RUN_ROOT/steps/step-002/step-summary.json" "$RUN_ROOT/summary.json" "$RP_RUN_MODE" "$OURS_EXTRA_ARGS"
|
||||
import json, sys
|
||||
steps = [json.load(open(p)) for p in sys.argv[1:3]]
|
||||
summary = {
|
||||
"workflowName": "性能对比测试快速版",
|
||||
"scope": "APNIC+ARIN mixed release two-step synchronized compare",
|
||||
"oursExtraArgs": sys.argv[4],
|
||||
"rpRunMode": sys.argv[4],
|
||||
"oursExtraArgs": sys.argv[5],
|
||||
"steps": steps,
|
||||
}
|
||||
json.dump(summary, open(sys.argv[3], "w"), indent=2, ensure_ascii=False)
|
||||
|
||||
@ -342,8 +342,8 @@ mod tests {
|
||||
use crate::audit::sha256_hex;
|
||||
use crate::data_model::roa::RoaObject;
|
||||
use crate::storage::{
|
||||
PackTime, ValidatedManifestMeta, VcirAuditSummary, VcirChildEntry, VcirInstanceGate,
|
||||
VcirRelatedArtifact, VcirSummary,
|
||||
PackTime, ValidatedManifestMeta, VcirAuditSummary, VcirCcrManifestProjection,
|
||||
VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||
};
|
||||
use base64::Engine as _;
|
||||
|
||||
@ -357,6 +357,19 @@ mod tests {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let next = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1));
|
||||
let local_outputs: Vec<VcirLocalOutput> = local_output.into_iter().collect();
|
||||
let ccr_manifest_projection = VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
manifest_sha256: vec![0x44; 32],
|
||||
manifest_size: 2048,
|
||||
manifest_ee_aki: vec![0x55; 20],
|
||||
manifest_number_be: vec![1],
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis: vec![vec![0x33; 20]],
|
||||
};
|
||||
ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
parent_manifest_rsync_uri: parent_manifest_rsync_uri.map(str::to_string),
|
||||
@ -372,6 +385,7 @@ mod tests {
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
||||
validated_manifest_next_update: next.clone(),
|
||||
},
|
||||
ccr_manifest_projection,
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: next.clone(),
|
||||
current_crl_next_update: next.clone(),
|
||||
@ -569,14 +583,12 @@ mod tests {
|
||||
);
|
||||
assert!(trace.source_object_raw.raw_present);
|
||||
assert!(trace.source_ee_cert_raw.raw_present);
|
||||
assert!(
|
||||
trace.chain_leaf_to_root[0]
|
||||
.related_artifacts
|
||||
.iter()
|
||||
.any(|artifact| {
|
||||
artifact.uri.as_deref() == Some(leaf_manifest) && artifact.raw.raw_present
|
||||
})
|
||||
);
|
||||
assert!(trace.chain_leaf_to_root[0]
|
||||
.related_artifacts
|
||||
.iter()
|
||||
.any(|artifact| {
|
||||
artifact.uri.as_deref() == Some(leaf_manifest) && artifact.raw.raw_present
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
415
src/ccr/accumulator.rs
Normal file
415
src/ccr/accumulator.rs
Normal file
@ -0,0 +1,415 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::ccr::build::{
|
||||
build_aspa_payload_state, build_roa_payload_state, build_router_key_state_from_runtime,
|
||||
build_trust_anchor_state,
|
||||
};
|
||||
use crate::ccr::encode::encode_manifest_state_payload_der;
|
||||
use crate::ccr::hash::compute_state_hash;
|
||||
use crate::ccr::model::{
|
||||
CcrDigestAlgorithm, ManifestInstance, ManifestState, RpkiCanonicalCacheRepresentation,
|
||||
};
|
||||
use crate::data_model::common::BigUnsigned;
|
||||
use crate::data_model::ta::TrustAnchor;
|
||||
use crate::storage::VcirCcrManifestProjection;
|
||||
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CcrManifestContribution {
|
||||
pub manifest_rsync_uri: String,
|
||||
pub hash: Vec<u8>,
|
||||
pub size: u64,
|
||||
pub aki: Vec<u8>,
|
||||
pub manifest_number_be: Vec<u8>,
|
||||
pub this_update: time::OffsetDateTime,
|
||||
pub locations_der: Vec<Vec<u8>>,
|
||||
pub subordinate_skis: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl CcrManifestContribution {
|
||||
fn from_projection(projection: &VcirCcrManifestProjection) -> Result<Self, String> {
|
||||
let this_update = projection
|
||||
.manifest_this_update
|
||||
.parse()
|
||||
.map_err(|e| format!("parse projection manifest_this_update failed: {e}"))?;
|
||||
Ok(Self {
|
||||
manifest_rsync_uri: projection.manifest_rsync_uri.clone(),
|
||||
hash: projection.manifest_sha256.clone(),
|
||||
size: projection.manifest_size,
|
||||
aki: projection.manifest_ee_aki.clone(),
|
||||
manifest_number_be: projection.manifest_number_be.clone(),
|
||||
this_update,
|
||||
locations_der: projection.manifest_sia_locations_der.clone(),
|
||||
subordinate_skis: projection.subordinate_skis.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
fn to_manifest_instance(&self) -> ManifestInstance {
|
||||
ManifestInstance {
|
||||
hash: self.hash.clone(),
|
||||
size: self.size,
|
||||
aki: self.aki.clone(),
|
||||
manifest_number: BigUnsigned {
|
||||
bytes_be: self.manifest_number_be.clone(),
|
||||
},
|
||||
this_update: self.this_update,
|
||||
locations: self.locations_der.clone(),
|
||||
subordinates: self.subordinate_skis.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CcrAccumulator {
|
||||
trust_anchors: Vec<TrustAnchor>,
|
||||
manifests_by_hash: BTreeMap<Vec<u8>, CcrManifestContribution>,
|
||||
most_recent_update: time::OffsetDateTime,
|
||||
}
|
||||
|
||||
impl CcrAccumulator {
|
||||
pub fn new(trust_anchors: Vec<TrustAnchor>) -> Self {
|
||||
Self {
|
||||
trust_anchors,
|
||||
manifests_by_hash: BTreeMap::new(),
|
||||
most_recent_update: time::OffsetDateTime::UNIX_EPOCH,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn append_manifest_projection(
|
||||
&mut self,
|
||||
projection: &VcirCcrManifestProjection,
|
||||
) -> Result<(), String> {
|
||||
let contribution = CcrManifestContribution::from_projection(projection)?;
|
||||
match self.manifests_by_hash.get(contribution.hash.as_slice()) {
|
||||
Some(existing) if existing != &contribution => {
|
||||
return Err(format!(
|
||||
"duplicate manifest hash with conflicting content for URI: {}",
|
||||
contribution.manifest_rsync_uri
|
||||
));
|
||||
}
|
||||
Some(_) => {}
|
||||
None => {
|
||||
self.manifests_by_hash
|
||||
.insert(contribution.hash.clone(), contribution.clone());
|
||||
}
|
||||
}
|
||||
if contribution.this_update > self.most_recent_update {
|
||||
self.most_recent_update = contribution.this_update;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn finish(
|
||||
&self,
|
||||
produced_at: time::OffsetDateTime,
|
||||
vrps: &[Vrp],
|
||||
aspas: &[AspaAttestation],
|
||||
router_keys: &[RouterKeyPayload],
|
||||
) -> Result<RpkiCanonicalCacheRepresentation, String> {
|
||||
let manifest_instances = self
|
||||
.manifests_by_hash
|
||||
.values()
|
||||
.map(CcrManifestContribution::to_manifest_instance)
|
||||
.collect::<Vec<_>>();
|
||||
let manifest_payload_der = encode_manifest_state_payload_der(&manifest_instances)
|
||||
.map_err(|e| format!("manifest state encoding failed: {e}"))?;
|
||||
let manifest_state = ManifestState {
|
||||
mis: manifest_instances,
|
||||
most_recent_update: self.most_recent_update,
|
||||
hash: compute_state_hash(&manifest_payload_der),
|
||||
};
|
||||
let vrp_state = build_roa_payload_state(vrps).map_err(|e| e.to_string())?;
|
||||
let aspa_state = build_aspa_payload_state(aspas).map_err(|e| e.to_string())?;
|
||||
let ta_state = build_trust_anchor_state(&self.trust_anchors).map_err(|e| e.to_string())?;
|
||||
let router_key_state =
|
||||
build_router_key_state_from_runtime(router_keys).map_err(|e| e.to_string())?;
|
||||
Ok(RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at,
|
||||
mfts: Some(manifest_state),
|
||||
vrps: Some(vrp_state),
|
||||
vaps: Some(aspa_state),
|
||||
tas: Some(ta_state),
|
||||
rks: Some(router_key_state),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn manifest_count(&self) -> usize {
|
||||
self.manifests_by_hash.len()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ccr::export::build_ccr_from_run;
|
||||
use crate::ccr::verify::verify_content_info;
|
||||
use crate::data_model::manifest::ManifestObject;
|
||||
use crate::data_model::rc::SubjectInfoAccess;
|
||||
use crate::data_model::roa::{IpPrefix, RoaAfi};
|
||||
use crate::data_model::ta::TrustAnchor;
|
||||
use crate::data_model::tal::Tal;
|
||||
use crate::storage::{
|
||||
PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
|
||||
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
|
||||
VcirCcrManifestProjection, VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact,
|
||||
VcirSummary,
|
||||
};
|
||||
use sha2::Digest;
|
||||
|
||||
fn sample_trust_anchor() -> TrustAnchor {
|
||||
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let tal_bytes = std::fs::read(base.join("tests/fixtures/tal/apnic-rfc7730-https.tal"))
|
||||
.expect("read tal");
|
||||
let ta_der = std::fs::read(base.join("tests/fixtures/ta/apnic-ta.cer")).expect("read ta");
|
||||
let tal = Tal::decode_bytes(&tal_bytes).expect("decode tal");
|
||||
TrustAnchor::bind_der(tal, &ta_der, None).expect("bind ta")
|
||||
}
|
||||
|
||||
fn sample_vcir_and_manifest(
|
||||
store: &RocksStore,
|
||||
) -> (
|
||||
ValidatedCaInstanceResult,
|
||||
Vec<Vrp>,
|
||||
Vec<AspaAttestation>,
|
||||
Vec<RouterKeyPayload>,
|
||||
) {
|
||||
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let manifest_der = std::fs::read(
|
||||
base.join(
|
||||
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
||||
),
|
||||
)
|
||||
.expect("read manifest");
|
||||
let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest");
|
||||
let manifest_hash = hex::encode(sha2::Sha256::digest(&manifest_der));
|
||||
let mut raw = RawByHashEntry::from_bytes(manifest_hash.clone(), manifest_der.clone());
|
||||
raw.origin_uris
|
||||
.push("rsync://example.test/repo/current.mft".to_string());
|
||||
raw.object_type = Some("mft".to_string());
|
||||
raw.encoding = Some("der".to_string());
|
||||
store.put_raw_by_hash_entry(&raw).expect("put raw");
|
||||
|
||||
let projection = VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
||||
manifest_sha256: sha2::Sha256::digest(&manifest_der).to_vec(),
|
||||
manifest_size: manifest_der.len() as u64,
|
||||
manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.authority_key_identifier
|
||||
.clone()
|
||||
.expect("manifest aki"),
|
||||
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
|
||||
manifest_sia_locations_der: match manifest.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.subject_info_access
|
||||
.as_ref()
|
||||
.expect("manifest sia")
|
||||
{
|
||||
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
||||
.access_descriptions
|
||||
.iter()
|
||||
.map(|ad| {
|
||||
let oid = encode_oid_for_test(&ad.access_method_oid)
|
||||
.expect("encode access method oid");
|
||||
let uri = encode_tlv_for_test(0x86, ad.access_location.as_bytes().to_vec());
|
||||
encode_sequence_for_test(&[oid, uri])
|
||||
})
|
||||
.collect(),
|
||||
SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
|
||||
},
|
||||
subordinate_skis: vec![vec![0x33; 20]],
|
||||
};
|
||||
|
||||
let vcir = ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
tal_id: "apnic".to_string(),
|
||||
ca_subject_name: "CN=test".to_string(),
|
||||
ca_ski: "11".repeat(20),
|
||||
issuer_ski: "22".repeat(20),
|
||||
last_successful_validation_time: PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.this_update,
|
||||
),
|
||||
current_manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
||||
current_crl_rsync_uri: "rsync://example.test/repo/current.crl".to_string(),
|
||||
validated_manifest_meta: ValidatedManifestMeta {
|
||||
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.this_update,
|
||||
),
|
||||
validated_manifest_next_update: PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.next_update,
|
||||
),
|
||||
},
|
||||
ccr_manifest_projection: projection.clone(),
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.next_update,
|
||||
),
|
||||
current_crl_next_update: PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.next_update,
|
||||
),
|
||||
self_ca_not_after: PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.next_update,
|
||||
),
|
||||
instance_effective_until: PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.next_update,
|
||||
),
|
||||
},
|
||||
child_entries: vec![VcirChildEntry {
|
||||
child_manifest_rsync_uri: "rsync://example.test/repo/child.mft".to_string(),
|
||||
child_cert_rsync_uri: "rsync://example.test/repo/child.cer".to_string(),
|
||||
child_cert_hash: "aa".repeat(32),
|
||||
child_ski: "33".repeat(20),
|
||||
child_rsync_base_uri: "rsync://example.test/repo/".to_string(),
|
||||
child_publication_point_rsync_uri: "rsync://example.test/repo/".to_string(),
|
||||
child_rrdp_notification_uri: None,
|
||||
child_effective_ip_resources: None,
|
||||
child_effective_as_resources: None,
|
||||
accepted_at_validation_time: PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.this_update,
|
||||
),
|
||||
}],
|
||||
local_outputs: Vec::new(),
|
||||
related_artifacts: vec![VcirRelatedArtifact {
|
||||
artifact_role: VcirArtifactRole::Manifest,
|
||||
artifact_kind: VcirArtifactKind::Mft,
|
||||
uri: Some("rsync://example.test/repo/current.mft".to_string()),
|
||||
sha256: manifest_hash,
|
||||
object_type: Some("mft".to_string()),
|
||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||
}],
|
||||
summary: VcirSummary {
|
||||
local_vrp_count: 0,
|
||||
local_aspa_count: 0,
|
||||
local_router_key_count: 0,
|
||||
child_count: 1,
|
||||
accepted_object_count: 1,
|
||||
rejected_object_count: 0,
|
||||
},
|
||||
audit_summary: VcirAuditSummary {
|
||||
failed_fetch_eligible: true,
|
||||
last_failed_fetch_reason: None,
|
||||
warning_count: 0,
|
||||
audit_flags: Vec::new(),
|
||||
},
|
||||
};
|
||||
|
||||
let vrps = vec![Vrp {
|
||||
asn: 64496,
|
||||
prefix: IpPrefix {
|
||||
afi: RoaAfi::Ipv4,
|
||||
prefix_len: 8,
|
||||
addr: [10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
},
|
||||
max_length: 8,
|
||||
}];
|
||||
let aspas = vec![AspaAttestation {
|
||||
customer_as_id: 64496,
|
||||
provider_as_ids: vec![64497],
|
||||
}];
|
||||
let router_keys = vec![RouterKeyPayload {
|
||||
as_id: 64496,
|
||||
ski: vec![0x11; 20],
|
||||
spki_der: vec![0x30, 0x00],
|
||||
source_object_uri: "rsync://example.test/repo/router.cer".to_string(),
|
||||
source_object_hash: hex::encode([0x11; 32]),
|
||||
source_ee_cert_hash: hex::encode([0x11; 32]),
|
||||
item_effective_until: PackTime::from_utc_offset_datetime(
|
||||
time::OffsetDateTime::now_utc() + time::Duration::hours(1),
|
||||
),
|
||||
}];
|
||||
(vcir, vrps, aspas, router_keys)
|
||||
}
|
||||
|
||||
fn encode_oid_for_test(oid: &str) -> Result<Vec<u8>, String> {
|
||||
let arcs = oid
|
||||
.split('.')
|
||||
.map(|part| part.parse::<u64>().map_err(|_| format!("bad oid: {oid}")))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
if arcs.len() < 2 {
|
||||
return Err(format!("bad oid: {oid}"));
|
||||
}
|
||||
let mut body = Vec::new();
|
||||
body.push((arcs[0] * 40 + arcs[1]) as u8);
|
||||
for arc in &arcs[2..] {
|
||||
encode_base128_for_test(*arc, &mut body);
|
||||
}
|
||||
Ok(encode_tlv_for_test(0x06, body))
|
||||
}
|
||||
|
||||
fn encode_base128_for_test(mut value: u64, out: &mut Vec<u8>) {
|
||||
let mut tmp = vec![(value & 0x7F) as u8];
|
||||
value >>= 7;
|
||||
while value > 0 {
|
||||
tmp.push(((value & 0x7F) as u8) | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
tmp.reverse();
|
||||
out.extend_from_slice(&tmp);
|
||||
}
|
||||
|
||||
fn encode_sequence_for_test(elements: &[Vec<u8>]) -> Vec<u8> {
|
||||
let total_len: usize = elements.iter().map(Vec::len).sum();
|
||||
let mut buf = Vec::with_capacity(total_len);
|
||||
for element in elements {
|
||||
buf.extend_from_slice(element);
|
||||
}
|
||||
encode_tlv_for_test(0x30, buf)
|
||||
}
|
||||
|
||||
fn encode_tlv_for_test(tag: u8, value: Vec<u8>) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(1 + 9 + value.len());
|
||||
out.push(tag);
|
||||
if value.len() < 0x80 {
|
||||
out.push(value.len() as u8);
|
||||
} else {
|
||||
out.push(0x81);
|
||||
out.push(value.len() as u8);
|
||||
}
|
||||
out.extend_from_slice(&value);
|
||||
out
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accumulator_finish_matches_builder_on_fresh_vcir_inputs() {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
||||
let (vcir, vrps, aspas, router_keys) = sample_vcir_and_manifest(&store);
|
||||
store.put_vcir(&vcir).expect("put vcir");
|
||||
let trust_anchor = sample_trust_anchor();
|
||||
|
||||
let builder_ccr = build_ccr_from_run(
|
||||
&store,
|
||||
&[trust_anchor.clone()],
|
||||
&vrps,
|
||||
&aspas,
|
||||
&router_keys,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
)
|
||||
.expect("build ccr from run");
|
||||
|
||||
let mut accumulator = CcrAccumulator::new(vec![trust_anchor]);
|
||||
accumulator
|
||||
.append_manifest_projection(&vcir.ccr_manifest_projection)
|
||||
.expect("append manifest projection");
|
||||
let accumulated_ccr = accumulator
|
||||
.finish(time::OffsetDateTime::now_utc(), &vrps, &aspas, &router_keys)
|
||||
.expect("finish accumulator");
|
||||
|
||||
assert_eq!(builder_ccr.mfts, accumulated_ccr.mfts);
|
||||
assert_eq!(builder_ccr.vrps, accumulated_ccr.vrps);
|
||||
assert_eq!(builder_ccr.vaps, accumulated_ccr.vaps);
|
||||
assert_eq!(builder_ccr.tas, accumulated_ccr.tas);
|
||||
assert_eq!(builder_ccr.rks, accumulated_ccr.rks);
|
||||
let ci = crate::ccr::model::CcrContentInfo::new(accumulated_ccr);
|
||||
verify_content_info(&ci).expect("verify accumulated ccr");
|
||||
}
|
||||
}
|
||||
119
src/ccr/build.rs
119
src/ccr/build.rs
@ -1,5 +1,6 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use serde::Serialize;
|
||||
use sha2::Digest;
|
||||
|
||||
use crate::ccr::encode::{
|
||||
@ -18,7 +19,7 @@ use crate::data_model::roa::RoaAfi;
|
||||
use crate::data_model::router_cert::BgpsecRouterCertificate;
|
||||
use crate::data_model::ta::TrustAnchor;
|
||||
use crate::storage::{RocksStore, ValidatedCaInstanceResult, VcirArtifactRole};
|
||||
use crate::validation::objects::{AspaAttestation, Vrp};
|
||||
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum CcrBuildError {
|
||||
@ -83,6 +84,19 @@ pub enum CcrBuildError {
|
||||
RouterKeyEncode(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct ManifestStateBuildBreakdown {
|
||||
pub vcir_count: usize,
|
||||
pub unique_manifest_count: usize,
|
||||
pub find_manifest_artifact_ms: u64,
|
||||
pub load_manifest_blob_ms: u64,
|
||||
pub decode_manifest_der_ms: u64,
|
||||
pub build_manifest_instance_ms: u64,
|
||||
pub dedup_manifest_instance_ms: u64,
|
||||
pub encode_manifest_state_ms: u64,
|
||||
pub total_ms: u64,
|
||||
}
|
||||
|
||||
pub fn build_roa_payload_state(vrps: &[Vrp]) -> Result<RoaPayloadState, CcrBuildError> {
|
||||
let mut grouped: BTreeMap<u32, BTreeSet<RoaPayloadKey>> = BTreeMap::new();
|
||||
for vrp in vrps {
|
||||
@ -174,10 +188,28 @@ pub fn build_manifest_state_from_vcirs(
|
||||
store: &RocksStore,
|
||||
vcirs: &[ValidatedCaInstanceResult],
|
||||
) -> Result<ManifestState, CcrBuildError> {
|
||||
build_manifest_state_from_vcirs_with_breakdown(store, vcirs).map(|(state, _)| state)
|
||||
}
|
||||
|
||||
pub fn build_manifest_state_from_vcirs_with_breakdown(
|
||||
store: &RocksStore,
|
||||
vcirs: &[ValidatedCaInstanceResult],
|
||||
) -> Result<(ManifestState, ManifestStateBuildBreakdown), CcrBuildError> {
|
||||
let total_started = std::time::Instant::now();
|
||||
let mut breakdown = ManifestStateBuildBreakdown {
|
||||
vcir_count: vcirs.len(),
|
||||
..ManifestStateBuildBreakdown::default()
|
||||
};
|
||||
let mut find_manifest_artifact_duration = std::time::Duration::ZERO;
|
||||
let mut load_manifest_blob_duration = std::time::Duration::ZERO;
|
||||
let mut decode_manifest_der_duration = std::time::Duration::ZERO;
|
||||
let mut build_manifest_instance_duration = std::time::Duration::ZERO;
|
||||
let mut dedup_manifest_instance_duration = std::time::Duration::ZERO;
|
||||
let mut mis_by_hash: BTreeMap<Vec<u8>, ManifestInstance> = BTreeMap::new();
|
||||
let mut most_recent_update = time::OffsetDateTime::UNIX_EPOCH;
|
||||
|
||||
for vcir in vcirs {
|
||||
let started = std::time::Instant::now();
|
||||
let manifest_artifact = vcir
|
||||
.related_artifacts
|
||||
.iter()
|
||||
@ -188,7 +220,9 @@ pub fn build_manifest_state_from_vcirs(
|
||||
.ok_or_else(|| {
|
||||
CcrBuildError::MissingManifestArtifact(vcir.current_manifest_rsync_uri.clone())
|
||||
})?;
|
||||
find_manifest_artifact_duration += started.elapsed();
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let raw_bytes = store
|
||||
.get_blob_bytes(&manifest_artifact.sha256)
|
||||
.map_err(|e| CcrBuildError::LoadManifestRawBytes {
|
||||
@ -199,13 +233,17 @@ pub fn build_manifest_state_from_vcirs(
|
||||
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
||||
sha256_hex: manifest_artifact.sha256.clone(),
|
||||
})?;
|
||||
load_manifest_blob_duration += started.elapsed();
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let manifest =
|
||||
ManifestObject::decode_der(&raw_bytes).map_err(|e| CcrBuildError::ManifestDecode {
|
||||
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
||||
detail: e.to_string(),
|
||||
})?;
|
||||
decode_manifest_der_duration += started.elapsed();
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let ee = &manifest.signed_object.signed_data.certificates[0].resource_cert;
|
||||
let aki = ee
|
||||
.tbs
|
||||
@ -263,7 +301,9 @@ pub fn build_manifest_state_from_vcirs(
|
||||
locations,
|
||||
subordinates,
|
||||
};
|
||||
build_manifest_instance_duration += started.elapsed();
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
match mis_by_hash.get(instance.hash.as_slice()) {
|
||||
Some(existing) if existing != &instance => {
|
||||
return Err(CcrBuildError::DuplicateManifestHashConflict(
|
||||
@ -275,16 +315,27 @@ pub fn build_manifest_state_from_vcirs(
|
||||
mis_by_hash.insert(instance.hash.clone(), instance);
|
||||
}
|
||||
}
|
||||
dedup_manifest_instance_duration += started.elapsed();
|
||||
}
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let mis: Vec<ManifestInstance> = mis_by_hash.into_values().collect();
|
||||
let payload_der = encode_manifest_state_payload_der(&mis)
|
||||
.map_err(|e| CcrBuildError::ManifestEncode(e.to_string()))?;
|
||||
Ok(ManifestState {
|
||||
let state = ManifestState {
|
||||
mis,
|
||||
most_recent_update,
|
||||
hash: compute_state_hash(&payload_der),
|
||||
})
|
||||
};
|
||||
breakdown.encode_manifest_state_ms = started.elapsed().as_millis() as u64;
|
||||
breakdown.find_manifest_artifact_ms = find_manifest_artifact_duration.as_millis() as u64;
|
||||
breakdown.load_manifest_blob_ms = load_manifest_blob_duration.as_millis() as u64;
|
||||
breakdown.decode_manifest_der_ms = decode_manifest_der_duration.as_millis() as u64;
|
||||
breakdown.build_manifest_instance_ms = build_manifest_instance_duration.as_millis() as u64;
|
||||
breakdown.dedup_manifest_instance_ms = dedup_manifest_instance_duration.as_millis() as u64;
|
||||
breakdown.unique_manifest_count = state.mis.len();
|
||||
breakdown.total_ms = total_started.elapsed().as_millis() as u64;
|
||||
Ok((state, breakdown))
|
||||
}
|
||||
|
||||
fn collect_subordinate_ski_bytes(
|
||||
@ -371,6 +422,36 @@ pub fn build_router_key_state(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn build_router_key_state_from_runtime(
|
||||
router_keys: &[RouterKeyPayload],
|
||||
) -> Result<RouterKeyState, CcrBuildError> {
|
||||
let mut grouped: BTreeMap<u32, BTreeSet<RouterKey>> = BTreeMap::new();
|
||||
for router_key in router_keys {
|
||||
grouped
|
||||
.entry(router_key.as_id)
|
||||
.or_default()
|
||||
.insert(RouterKey {
|
||||
ski: router_key.ski.clone(),
|
||||
spki_der: router_key.spki_der.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let rksets: Vec<RouterKeySet> = grouped
|
||||
.into_iter()
|
||||
.map(|(as_id, router_keys)| RouterKeySet {
|
||||
as_id,
|
||||
router_keys: router_keys.into_iter().collect(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let payload_der = encode_router_key_state_payload_der(&rksets)
|
||||
.map_err(|e| CcrBuildError::RouterKeyEncode(e.to_string()))?;
|
||||
Ok(RouterKeyState {
|
||||
rksets,
|
||||
hash: compute_state_hash(&payload_der),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
struct RoaPayloadKey {
|
||||
afi: u16,
|
||||
@ -578,6 +659,37 @@ mod tests {
|
||||
let hash = hex::encode(sha2::Sha256::digest(manifest_der));
|
||||
let now = manifest.manifest.this_update;
|
||||
let next = manifest.manifest.next_update;
|
||||
let projection = crate::storage::VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: manifest_uri.to_string(),
|
||||
manifest_sha256: sha2::Sha256::digest(manifest_der).to_vec(),
|
||||
manifest_size: manifest_der.len() as u64,
|
||||
manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.authority_key_identifier
|
||||
.clone()
|
||||
.expect("manifest aki"),
|
||||
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||
manifest_this_update: crate::storage::PackTime::from_utc_offset_datetime(now),
|
||||
manifest_sia_locations_der: match manifest.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.subject_info_access
|
||||
.as_ref()
|
||||
.expect("manifest sia")
|
||||
{
|
||||
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
||||
.access_descriptions
|
||||
.iter()
|
||||
.map(encode_access_description_der)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("encode access descriptions"),
|
||||
SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
|
||||
},
|
||||
subordinate_skis: vec![hex::decode(child_ski_hex).expect("decode child ski")],
|
||||
};
|
||||
crate::storage::ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: manifest_uri.to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
@ -599,6 +711,7 @@ mod tests {
|
||||
next,
|
||||
),
|
||||
},
|
||||
ccr_manifest_projection: projection,
|
||||
instance_gate: crate::storage::VcirInstanceGate {
|
||||
manifest_next_update: crate::storage::PackTime::from_utc_offset_datetime(next),
|
||||
current_crl_next_update: crate::storage::PackTime::from_utc_offset_datetime(next),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::ccr::model::{
|
||||
AspaPayloadSet, AspaPayloadState, CCR_VERSION_V0, CcrContentInfo, CcrDigestAlgorithm,
|
||||
ManifestInstance, ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet,
|
||||
RouterKeyState, RpkiCanonicalCacheRepresentation, TrustAnchorState,
|
||||
AspaPayloadSet, AspaPayloadState, CcrContentInfo, CcrDigestAlgorithm, ManifestInstance,
|
||||
ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState,
|
||||
RpkiCanonicalCacheRepresentation, TrustAnchorState, CCR_VERSION_V0,
|
||||
};
|
||||
use crate::data_model::common::{BigUnsigned, DerReader};
|
||||
use crate::data_model::oid::{OID_CT_RPKI_CCR, OID_CT_RPKI_CCR_RAW, OID_SHA256, OID_SHA256_RAW};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::ccr::decode::{CcrDecodeError, decode_content_info};
|
||||
use crate::ccr::decode::{decode_content_info, CcrDecodeError};
|
||||
use serde_json::json;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::ccr::model::{
|
||||
AspaPayloadSet, AspaPayloadState, CCR_VERSION_V0, CcrContentInfo, CcrDigestAlgorithm,
|
||||
ManifestInstance, ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet,
|
||||
RouterKeyState, RpkiCanonicalCacheRepresentation, TrustAnchorState,
|
||||
AspaPayloadSet, AspaPayloadState, CcrContentInfo, CcrDigestAlgorithm, ManifestInstance,
|
||||
ManifestState, RoaPayloadSet, RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState,
|
||||
RpkiCanonicalCacheRepresentation, TrustAnchorState, CCR_VERSION_V0,
|
||||
};
|
||||
use crate::data_model::common::BigUnsigned;
|
||||
use crate::data_model::oid::{OID_CT_RPKI_CCR_RAW, OID_SHA256_RAW};
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
use crate::ccr::build::{
|
||||
CcrBuildError, build_aspa_payload_state, build_manifest_state_from_vcirs,
|
||||
build_roa_payload_state, build_trust_anchor_state,
|
||||
build_aspa_payload_state, build_manifest_state_from_vcirs_with_breakdown,
|
||||
build_roa_payload_state, build_router_key_state_from_runtime, build_trust_anchor_state,
|
||||
CcrBuildError, ManifestStateBuildBreakdown,
|
||||
};
|
||||
use crate::ccr::encode::{CcrEncodeError, encode_content_info};
|
||||
use crate::ccr::encode::{encode_content_info, CcrEncodeError};
|
||||
use crate::ccr::model::{CcrContentInfo, CcrDigestAlgorithm, RpkiCanonicalCacheRepresentation};
|
||||
use crate::data_model::ta::TrustAnchor;
|
||||
use crate::storage::RocksStore;
|
||||
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
|
||||
use serde::Serialize;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@ -24,6 +26,19 @@ pub enum CcrExportError {
|
||||
Write(String, String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)]
|
||||
pub struct CcrBuildBreakdown {
|
||||
pub vcir_count: usize,
|
||||
pub list_vcirs_ms: u64,
|
||||
pub manifest_state_ms: u64,
|
||||
pub manifest_state_breakdown: ManifestStateBuildBreakdown,
|
||||
pub roa_payload_state_ms: u64,
|
||||
pub aspa_payload_state_ms: u64,
|
||||
pub trust_anchor_state_ms: u64,
|
||||
pub router_key_state_ms: u64,
|
||||
pub total_ms: u64,
|
||||
}
|
||||
|
||||
pub fn build_ccr_from_run(
|
||||
store: &RocksStore,
|
||||
trust_anchors: &[TrustAnchor],
|
||||
@ -32,63 +47,63 @@ pub fn build_ccr_from_run(
|
||||
router_keys: &[RouterKeyPayload],
|
||||
produced_at: time::OffsetDateTime,
|
||||
) -> Result<RpkiCanonicalCacheRepresentation, CcrExportError> {
|
||||
build_ccr_from_run_with_breakdown(store, trust_anchors, vrps, aspas, router_keys, produced_at)
|
||||
.map(|(ccr, _)| ccr)
|
||||
}
|
||||
|
||||
pub fn build_ccr_from_run_with_breakdown(
|
||||
store: &RocksStore,
|
||||
trust_anchors: &[TrustAnchor],
|
||||
vrps: &[Vrp],
|
||||
aspas: &[AspaAttestation],
|
||||
router_keys: &[RouterKeyPayload],
|
||||
produced_at: time::OffsetDateTime,
|
||||
) -> Result<(RpkiCanonicalCacheRepresentation, CcrBuildBreakdown), CcrExportError> {
|
||||
let total_started = std::time::Instant::now();
|
||||
let mut breakdown = CcrBuildBreakdown::default();
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let vcirs = store
|
||||
.list_vcirs()
|
||||
.map_err(|e| CcrExportError::ListVcirs(e.to_string()))?;
|
||||
breakdown.list_vcirs_ms = started.elapsed().as_millis() as u64;
|
||||
breakdown.vcir_count = vcirs.len();
|
||||
|
||||
let mfts = Some(build_manifest_state_from_vcirs(store, &vcirs)?);
|
||||
let vrps = Some(build_roa_payload_state(vrps)?);
|
||||
let vaps = Some(build_aspa_payload_state(aspas)?);
|
||||
let tas = Some(build_trust_anchor_state(trust_anchors)?);
|
||||
let rks = Some(build_router_key_state_from_runtime(router_keys)?);
|
||||
let started = std::time::Instant::now();
|
||||
let (mfts, manifest_state_breakdown) =
|
||||
build_manifest_state_from_vcirs_with_breakdown(store, &vcirs)?;
|
||||
breakdown.manifest_state_ms = started.elapsed().as_millis() as u64;
|
||||
breakdown.manifest_state_breakdown = manifest_state_breakdown;
|
||||
|
||||
Ok(RpkiCanonicalCacheRepresentation {
|
||||
let started = std::time::Instant::now();
|
||||
let vrps = build_roa_payload_state(vrps)?;
|
||||
breakdown.roa_payload_state_ms = started.elapsed().as_millis() as u64;
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let vaps = build_aspa_payload_state(aspas)?;
|
||||
breakdown.aspa_payload_state_ms = started.elapsed().as_millis() as u64;
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let tas = build_trust_anchor_state(trust_anchors)?;
|
||||
breakdown.trust_anchor_state_ms = started.elapsed().as_millis() as u64;
|
||||
|
||||
let started = std::time::Instant::now();
|
||||
let rks = build_router_key_state_from_runtime(router_keys)?;
|
||||
breakdown.router_key_state_ms = started.elapsed().as_millis() as u64;
|
||||
|
||||
let ccr = RpkiCanonicalCacheRepresentation {
|
||||
version: 0,
|
||||
hash_alg: CcrDigestAlgorithm::Sha256,
|
||||
produced_at,
|
||||
mfts,
|
||||
vrps,
|
||||
vaps,
|
||||
tas,
|
||||
rks,
|
||||
})
|
||||
}
|
||||
mfts: Some(mfts),
|
||||
vrps: Some(vrps),
|
||||
vaps: Some(vaps),
|
||||
tas: Some(tas),
|
||||
rks: Some(rks),
|
||||
};
|
||||
breakdown.total_ms = total_started.elapsed().as_millis() as u64;
|
||||
|
||||
fn build_router_key_state_from_runtime(
|
||||
router_keys: &[RouterKeyPayload],
|
||||
) -> Result<crate::ccr::model::RouterKeyState, CcrBuildError> {
|
||||
use crate::ccr::model::{RouterKey, RouterKeySet};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
let mut grouped: BTreeMap<u32, BTreeSet<RouterKey>> = BTreeMap::new();
|
||||
for router_key in router_keys {
|
||||
grouped
|
||||
.entry(router_key.as_id)
|
||||
.or_default()
|
||||
.insert(RouterKey {
|
||||
ski: router_key.ski.clone(),
|
||||
spki_der: router_key.spki_der.clone(),
|
||||
});
|
||||
}
|
||||
let rksets = grouped
|
||||
.into_iter()
|
||||
.map(|(as_id, router_keys)| RouterKeySet {
|
||||
as_id,
|
||||
router_keys: router_keys.into_iter().collect(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
build_router_key_state_from_sets(&rksets)
|
||||
}
|
||||
|
||||
fn build_router_key_state_from_sets(
|
||||
rksets: &[crate::ccr::model::RouterKeySet],
|
||||
) -> Result<crate::ccr::model::RouterKeyState, CcrBuildError> {
|
||||
let der = crate::ccr::encode::encode_router_key_state_payload_der(rksets)
|
||||
.map_err(|e| CcrBuildError::RouterKeyEncode(e.to_string()))?;
|
||||
Ok(crate::ccr::model::RouterKeyState {
|
||||
rksets: rksets.to_vec(),
|
||||
hash: crate::ccr::compute_state_hash(&der),
|
||||
})
|
||||
Ok((ccr, breakdown))
|
||||
}
|
||||
|
||||
pub fn write_ccr_file(
|
||||
@ -115,7 +130,8 @@ mod tests {
|
||||
use crate::storage::{
|
||||
PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta,
|
||||
VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
|
||||
VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||
VcirCcrManifestProjection, VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact,
|
||||
VcirSummary,
|
||||
};
|
||||
use crate::validation::objects::{AspaAttestation, RouterKeyPayload, Vrp};
|
||||
use sha2::Digest;
|
||||
@ -140,6 +156,25 @@ mod tests {
|
||||
raw.object_type = Some("mft".to_string());
|
||||
raw.encoding = Some("der".to_string());
|
||||
store.put_raw_by_hash_entry(&raw).expect("put raw");
|
||||
let projection = VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
||||
manifest_sha256: sha2::Sha256::digest(&manifest_der).to_vec(),
|
||||
manifest_size: manifest_der.len() as u64,
|
||||
manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.authority_key_identifier
|
||||
.clone()
|
||||
.expect("manifest aki"),
|
||||
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis: vec![vec![0x33; 20]],
|
||||
};
|
||||
ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
@ -161,6 +196,7 @@ mod tests {
|
||||
manifest.manifest.next_update,
|
||||
),
|
||||
},
|
||||
ccr_manifest_projection: projection,
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: PackTime::from_utc_offset_datetime(
|
||||
manifest.manifest.next_update,
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
#[cfg(feature = "full")]
|
||||
pub mod accumulator;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod build;
|
||||
pub mod decode;
|
||||
pub mod dump;
|
||||
@ -11,15 +13,22 @@ pub mod model;
|
||||
pub mod verify;
|
||||
|
||||
#[cfg(feature = "full")]
|
||||
pub use build::{
|
||||
CcrBuildError, build_aspa_payload_state, build_manifest_state_from_vcirs,
|
||||
build_roa_payload_state, build_trust_anchor_state,
|
||||
};
|
||||
pub use decode::{CcrDecodeError, decode_content_info};
|
||||
pub use dump::{CcrDumpError, dump_content_info_json, dump_content_info_json_value};
|
||||
pub use encode::{CcrEncodeError, encode_content_info};
|
||||
pub use accumulator::{CcrAccumulator, CcrManifestContribution};
|
||||
#[cfg(feature = "full")]
|
||||
pub use export::{CcrExportError, build_ccr_from_run, write_ccr_file};
|
||||
pub use build::{
|
||||
build_aspa_payload_state, build_manifest_state_from_vcirs,
|
||||
build_manifest_state_from_vcirs_with_breakdown, build_roa_payload_state,
|
||||
build_router_key_state_from_runtime, build_trust_anchor_state, CcrBuildError,
|
||||
ManifestStateBuildBreakdown,
|
||||
};
|
||||
pub use decode::{decode_content_info, CcrDecodeError};
|
||||
pub use dump::{dump_content_info_json, dump_content_info_json_value, CcrDumpError};
|
||||
pub use encode::{encode_content_info, CcrEncodeError};
|
||||
#[cfg(feature = "full")]
|
||||
pub use export::{
|
||||
build_ccr_from_run, build_ccr_from_run_with_breakdown, write_ccr_file, CcrBuildBreakdown,
|
||||
CcrExportError,
|
||||
};
|
||||
pub use hash::{compute_state_hash, verify_state_hash};
|
||||
pub use model::{
|
||||
AspaPayloadSet, AspaPayloadState, CcrContentInfo, CcrDigestAlgorithm, ManifestInstance,
|
||||
@ -28,7 +37,7 @@ pub use model::{
|
||||
};
|
||||
#[cfg(feature = "full")]
|
||||
pub use verify::{
|
||||
CcrVerifyError, CcrVerifySummary, extract_vrp_rows, verify_against_report_json_path,
|
||||
verify_against_vcir_store, verify_against_vcir_store_path, verify_content_info,
|
||||
verify_content_info_bytes,
|
||||
extract_vrp_rows, verify_against_report_json_path, verify_against_vcir_store,
|
||||
verify_against_vcir_store_path, verify_content_info, verify_content_info_bytes, CcrVerifyError,
|
||||
CcrVerifySummary,
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::data_model::common::{BigUnsigned, der_take_tlv};
|
||||
use crate::data_model::common::{der_take_tlv, BigUnsigned};
|
||||
use crate::data_model::oid::{OID_CT_RPKI_CCR, OID_SHA256};
|
||||
|
||||
pub const CCR_VERSION_V0: u32 = 0;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::ccr::decode::{CcrDecodeError, decode_content_info};
|
||||
use crate::ccr::decode::{decode_content_info, CcrDecodeError};
|
||||
use crate::ccr::encode::{
|
||||
encode_aspa_payload_state_payload_der, encode_manifest_state_payload_der,
|
||||
encode_roa_payload_state_payload_der, encode_router_key_state_payload_der,
|
||||
@ -430,8 +430,9 @@ mod tests {
|
||||
use crate::data_model::roa::{IpPrefix, RoaAfi};
|
||||
use crate::storage::{
|
||||
PackTime, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind,
|
||||
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirChildEntry,
|
||||
VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary,
|
||||
VcirCcrManifestProjection, VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact,
|
||||
VcirSummary,
|
||||
};
|
||||
use crate::validation::objects::{AspaAttestation, Vrp};
|
||||
|
||||
@ -505,6 +506,22 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
fn sample_ccr_manifest_projection(manifest_rsync_uri: &str) -> VcirCcrManifestProjection {
|
||||
VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
manifest_sha256: vec![0x44; 32],
|
||||
manifest_size: 2048,
|
||||
manifest_ee_aki: vec![0x55; 20],
|
||||
manifest_number_be: vec![1],
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis: vec![vec![0x33; 20]],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_detects_each_state_hash_mismatch() {
|
||||
let mut ci = sample_content_info();
|
||||
@ -599,6 +616,9 @@ mod tests {
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
validated_manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
},
|
||||
ccr_manifest_projection: sample_ccr_manifest_projection(
|
||||
"rsync://example.test/current.mft",
|
||||
),
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
current_crl_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
|
||||
623
src/cli.rs
623
src/cli.rs
@ -1,11 +1,14 @@
|
||||
use crate::ccr::{build_ccr_from_run, write_ccr_file};
|
||||
use crate::cir::{CirTalBinding, export_cir_from_run_multi};
|
||||
use crate::ccr::{
|
||||
build_ccr_from_run_with_breakdown, write_ccr_file, CcrAccumulator, CcrBuildBreakdown,
|
||||
};
|
||||
use crate::cir::{export_cir_from_run_multi, CirTalBinding};
|
||||
use std::io::BufWriter;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use crate::analysis::timing::{TimingHandle, TimingMeta, TimingMetaUpdate};
|
||||
use crate::audit::{
|
||||
AspaOutput, AuditRepoSyncStats, AuditReportV2, AuditRunMeta, AuditWarning, TreeSummary,
|
||||
VrpOutput, format_roa_ip_prefix,
|
||||
format_roa_ip_prefix, AspaOutput, AuditRepoSyncStats, AuditReportV2, AuditRunMeta,
|
||||
AuditWarning, TreeSummary, VrpOutput,
|
||||
};
|
||||
use crate::fetch::http::{BlockingHttpFetcher, HttpFetcherConfig};
|
||||
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
||||
@ -15,7 +18,7 @@ use crate::parallel::types::TalInputSpec;
|
||||
use crate::policy::Policy;
|
||||
use crate::storage::RocksStore;
|
||||
use crate::validation::run_tree_from_tal::{
|
||||
RunTreeFromTalAuditOutput, run_tree_from_multiple_tals_parallel_phase1_audit,
|
||||
run_tree_from_multiple_tals_parallel_phase1_audit,
|
||||
run_tree_from_multiple_tals_parallel_phase2_audit,
|
||||
run_tree_from_tal_and_ta_der_parallel_phase1_audit,
|
||||
run_tree_from_tal_and_ta_der_parallel_phase2_audit,
|
||||
@ -27,6 +30,7 @@ use crate::validation::run_tree_from_tal::{
|
||||
run_tree_from_tal_and_ta_der_serial_audit_with_timing,
|
||||
run_tree_from_tal_url_parallel_phase1_audit, run_tree_from_tal_url_parallel_phase2_audit,
|
||||
run_tree_from_tal_url_serial_audit, run_tree_from_tal_url_serial_audit_with_timing,
|
||||
RunTreeFromTalAuditOutput,
|
||||
};
|
||||
use crate::validation::tree::TreeRunConfig;
|
||||
use serde::Serialize;
|
||||
@ -38,6 +42,7 @@ struct RunStageTiming {
|
||||
report_build_ms: u64,
|
||||
report_write_ms: Option<u64>,
|
||||
ccr_build_ms: Option<u64>,
|
||||
ccr_build_breakdown: Option<CcrBuildBreakdown>,
|
||||
ccr_write_ms: Option<u64>,
|
||||
cir_build_cir_ms: Option<u64>,
|
||||
cir_write_cir_ms: Option<u64>,
|
||||
@ -71,6 +76,7 @@ pub struct CliArgs {
|
||||
pub raw_store_db: Option<PathBuf>,
|
||||
pub policy_path: Option<PathBuf>,
|
||||
pub report_json_path: Option<PathBuf>,
|
||||
pub report_json_compact: bool,
|
||||
pub ccr_out_path: Option<PathBuf>,
|
||||
pub cir_enabled: bool,
|
||||
pub cir_out_path: Option<PathBuf>,
|
||||
@ -114,6 +120,7 @@ Options:
|
||||
--raw-store-db <path> External raw-by-hash store DB path (optional)
|
||||
--policy <path> Policy TOML path (optional)
|
||||
--report-json <path> Write full audit report as JSON (optional)
|
||||
--report-json-compact Write report JSON without pretty-printing (requires --report-json)
|
||||
--ccr-out <path> Write CCR DER ContentInfo to this path (optional)
|
||||
--cir-enable Export CIR after the run completes
|
||||
--cir-out <path> Write CIR DER to this path (requires --cir-enable)
|
||||
@ -175,6 +182,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
||||
let mut raw_store_db: Option<PathBuf> = None;
|
||||
let mut policy_path: Option<PathBuf> = None;
|
||||
let mut report_json_path: Option<PathBuf> = None;
|
||||
let mut report_json_compact: bool = false;
|
||||
let mut ccr_out_path: Option<PathBuf> = None;
|
||||
let mut cir_enabled: bool = false;
|
||||
let mut cir_out_path: Option<PathBuf> = None;
|
||||
@ -298,6 +306,9 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
||||
let v = argv.get(i).ok_or("--report-json requires a value")?;
|
||||
report_json_path = Some(PathBuf::from(v));
|
||||
}
|
||||
"--report-json-compact" => {
|
||||
report_json_compact = true;
|
||||
}
|
||||
"--ccr-out" => {
|
||||
i += 1;
|
||||
let v = argv.get(i).ok_or("--ccr-out requires a value")?;
|
||||
@ -514,6 +525,12 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
||||
if cir_enabled && cir_out_path.is_none() {
|
||||
return Err(format!("--cir-enable requires --cir-out\n\n{}", usage()));
|
||||
}
|
||||
if report_json_compact && report_json_path.is_none() {
|
||||
return Err(format!(
|
||||
"--report-json-compact requires --report-json\n\n{}",
|
||||
usage()
|
||||
));
|
||||
}
|
||||
if cir_static_root.is_some() {
|
||||
return Err(format!(
|
||||
"--cir-static-root is no longer supported; CIR export now writes only .cir files\n\n{}",
|
||||
@ -660,6 +677,7 @@ pub fn parse_args(argv: &[String]) -> Result<CliArgs, String> {
|
||||
raw_store_db,
|
||||
policy_path,
|
||||
report_json_path,
|
||||
report_json_compact,
|
||||
ccr_out_path,
|
||||
cir_enabled,
|
||||
cir_out_path,
|
||||
@ -698,11 +716,21 @@ fn read_policy(path: Option<&Path>) -> Result<Policy, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, report: &AuditReportV2) -> Result<(), String> {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum ReportJsonFormat {
|
||||
Pretty,
|
||||
Compact,
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, report: &AuditReportV2, format: ReportJsonFormat) -> Result<(), String> {
|
||||
let f = std::fs::File::create(path)
|
||||
.map_err(|e| format!("create report file failed: {}: {e}", path.display()))?;
|
||||
serde_json::to_writer_pretty(f, report)
|
||||
.map_err(|e| format!("write report json failed: {e}"))?;
|
||||
let writer = BufWriter::new(f);
|
||||
match format {
|
||||
ReportJsonFormat::Pretty => serde_json::to_writer_pretty(writer, report),
|
||||
ReportJsonFormat::Compact => serde_json::to_writer(writer, report),
|
||||
}
|
||||
.map_err(|e| format!("write report json failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -746,10 +774,85 @@ fn print_summary(report: &AuditReportV2) {
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct PostValidationShared {
|
||||
discovery: crate::validation::from_tal::DiscoveredRootCaInstance,
|
||||
discoveries: Arc<[crate::validation::from_tal::DiscoveredRootCaInstance]>,
|
||||
instances_processed: usize,
|
||||
instances_failed: usize,
|
||||
tree_warnings: Arc<[crate::report::Warning]>,
|
||||
vrps: Arc<[crate::validation::objects::Vrp]>,
|
||||
aspas: Arc<[crate::validation::objects::AspaAttestation]>,
|
||||
router_keys: Arc<[crate::validation::objects::RouterKeyPayload]>,
|
||||
publication_points: Arc<[crate::audit::PublicationPointAudit]>,
|
||||
downloads: Arc<[crate::audit::AuditDownloadEvent]>,
|
||||
download_stats: crate::audit::AuditDownloadStats,
|
||||
current_repo_objects: Arc<[crate::current_repo_index::CurrentRepoObject]>,
|
||||
ccr_accumulator: Option<CcrAccumulator>,
|
||||
}
|
||||
|
||||
impl PostValidationShared {
|
||||
fn from_run_output(out: RunTreeFromTalAuditOutput) -> Self {
|
||||
let RunTreeFromTalAuditOutput {
|
||||
discovery,
|
||||
discoveries,
|
||||
tree,
|
||||
publication_points,
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects,
|
||||
ccr_accumulator,
|
||||
} = out;
|
||||
let crate::validation::tree::TreeRunOutput {
|
||||
instances_processed,
|
||||
instances_failed,
|
||||
warnings,
|
||||
vrps,
|
||||
aspas,
|
||||
router_keys,
|
||||
} = tree;
|
||||
|
||||
Self {
|
||||
discovery,
|
||||
discoveries: discoveries.into(),
|
||||
instances_processed,
|
||||
instances_failed,
|
||||
tree_warnings: warnings.into(),
|
||||
vrps: vrps.into(),
|
||||
aspas: aspas.into(),
|
||||
router_keys: router_keys.into(),
|
||||
publication_points: publication_points.into(),
|
||||
downloads: downloads.into(),
|
||||
download_stats,
|
||||
current_repo_objects: current_repo_objects.into(),
|
||||
ccr_accumulator,
|
||||
}
|
||||
}
|
||||
|
||||
fn trust_anchors(&self) -> Vec<crate::data_model::ta::TrustAnchor> {
|
||||
if self.discoveries.is_empty() {
|
||||
vec![self.discovery.trust_anchor.clone()]
|
||||
} else {
|
||||
self.discoveries
|
||||
.iter()
|
||||
.map(|item| item.trust_anchor.clone())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn current_repo_objects(&self) -> Option<&[crate::current_repo_index::CurrentRepoObject]> {
|
||||
if self.current_repo_objects.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(self.current_repo_objects.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_report(
|
||||
policy: &Policy,
|
||||
validation_time: time::OffsetDateTime,
|
||||
out: RunTreeFromTalAuditOutput,
|
||||
shared: &PostValidationShared,
|
||||
) -> AuditReportV2 {
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
let validation_time_rfc3339_utc = validation_time
|
||||
@ -757,8 +860,7 @@ fn build_report(
|
||||
.format(&Rfc3339)
|
||||
.expect("format validation_time");
|
||||
|
||||
let vrps = out
|
||||
.tree
|
||||
let vrps = shared
|
||||
.vrps
|
||||
.iter()
|
||||
.map(|v| VrpOutput {
|
||||
@ -768,8 +870,7 @@ fn build_report(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let aspas = out
|
||||
.tree
|
||||
let aspas = shared
|
||||
.aspas
|
||||
.iter()
|
||||
.map(|a| AspaOutput {
|
||||
@ -778,7 +879,7 @@ fn build_report(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let repo_sync_stats = build_repo_sync_stats(&out.publication_points);
|
||||
let repo_sync_stats = build_repo_sync_stats(shared.publication_points.as_ref());
|
||||
|
||||
AuditReportV2 {
|
||||
format_version: 2,
|
||||
@ -787,19 +888,137 @@ fn build_report(
|
||||
},
|
||||
policy: policy.clone(),
|
||||
tree: TreeSummary {
|
||||
instances_processed: out.tree.instances_processed,
|
||||
instances_failed: out.tree.instances_failed,
|
||||
warnings: out.tree.warnings.iter().map(AuditWarning::from).collect(),
|
||||
instances_processed: shared.instances_processed,
|
||||
instances_failed: shared.instances_failed,
|
||||
warnings: shared
|
||||
.tree_warnings
|
||||
.iter()
|
||||
.map(AuditWarning::from)
|
||||
.collect(),
|
||||
},
|
||||
publication_points: out.publication_points,
|
||||
publication_points: shared.publication_points.iter().cloned().collect(),
|
||||
vrps,
|
||||
aspas,
|
||||
downloads: out.downloads,
|
||||
download_stats: out.download_stats,
|
||||
downloads: shared.downloads.iter().cloned().collect(),
|
||||
download_stats: shared.download_stats.clone(),
|
||||
repo_sync_stats,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct ReportTaskOutput {
|
||||
report: AuditReportV2,
|
||||
report_build_ms: u64,
|
||||
report_write_ms: Option<u64>,
|
||||
}
|
||||
|
||||
fn run_report_task(
|
||||
policy: &Policy,
|
||||
validation_time: time::OffsetDateTime,
|
||||
shared: &PostValidationShared,
|
||||
report_json_path: Option<&Path>,
|
||||
report_json_format: ReportJsonFormat,
|
||||
) -> Result<ReportTaskOutput, String> {
|
||||
let report_started = std::time::Instant::now();
|
||||
let report = build_report(policy, validation_time, shared);
|
||||
let report_build_ms = report_started.elapsed().as_millis() as u64;
|
||||
|
||||
let report_write_ms = if let Some(path) = report_json_path {
|
||||
let started = std::time::Instant::now();
|
||||
write_json(path, &report, report_json_format)?;
|
||||
Some(started.elapsed().as_millis() as u64)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ReportTaskOutput {
|
||||
report,
|
||||
report_build_ms,
|
||||
report_write_ms,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct CcrTaskOutput {
|
||||
ccr_build_ms: Option<u64>,
|
||||
ccr_build_breakdown: Option<CcrBuildBreakdown>,
|
||||
ccr_write_ms: Option<u64>,
|
||||
}
|
||||
|
||||
fn run_ccr_task(
|
||||
store: &RocksStore,
|
||||
shared: &PostValidationShared,
|
||||
ccr_out_path: Option<&Path>,
|
||||
produced_at: time::OffsetDateTime,
|
||||
) -> Result<CcrTaskOutput, String> {
|
||||
let mut ccr_build_ms = None;
|
||||
let mut ccr_build_breakdown = None;
|
||||
let mut ccr_write_ms = None;
|
||||
if let Some(path) = ccr_out_path {
|
||||
let started = std::time::Instant::now();
|
||||
let (ccr, build_breakdown) = if let Some(accumulator) = shared.ccr_accumulator.as_ref() {
|
||||
(
|
||||
accumulator
|
||||
.finish(
|
||||
produced_at,
|
||||
shared.vrps.as_ref(),
|
||||
shared.aspas.as_ref(),
|
||||
shared.router_keys.as_ref(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?,
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
let trust_anchors = shared.trust_anchors();
|
||||
let (ccr, build_breakdown) = build_ccr_from_run_with_breakdown(
|
||||
store,
|
||||
&trust_anchors,
|
||||
shared.vrps.as_ref(),
|
||||
shared.aspas.as_ref(),
|
||||
shared.router_keys.as_ref(),
|
||||
produced_at,
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
(ccr, Some(build_breakdown))
|
||||
};
|
||||
ccr_build_ms = Some(started.elapsed().as_millis() as u64);
|
||||
ccr_build_breakdown = build_breakdown;
|
||||
let started = std::time::Instant::now();
|
||||
write_ccr_file(path, &ccr).map_err(|e| e.to_string())?;
|
||||
ccr_write_ms = Some(started.elapsed().as_millis() as u64);
|
||||
eprintln!("wrote CCR: {}", path.display());
|
||||
}
|
||||
|
||||
Ok(CcrTaskOutput {
|
||||
ccr_build_ms,
|
||||
ccr_build_breakdown,
|
||||
ccr_write_ms,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_stage_timing(
|
||||
report_json_path: Option<&Path>,
|
||||
stage_timing: &RunStageTiming,
|
||||
) -> Result<(), String> {
|
||||
if let Some(path) = report_json_path {
|
||||
if let Some(parent) = path.parent() {
|
||||
let stage_timing_path = parent.join("stage-timing.json");
|
||||
std::fs::write(
|
||||
&stage_timing_path,
|
||||
serde_json::to_vec_pretty(stage_timing).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"write stage timing failed: {}: {e}",
|
||||
stage_timing_path.display()
|
||||
)
|
||||
})?;
|
||||
eprintln!("analysis: wrote {}", stage_timing_path.display());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_cir_export_tal_uris(args: &CliArgs) -> Result<Vec<String>, String> {
|
||||
if !args.cir_tal_uris.is_empty() {
|
||||
return Ok(args.cir_tal_uris.clone());
|
||||
@ -1468,37 +1687,39 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
||||
};
|
||||
|
||||
let validation_ms = validation_started.elapsed().as_millis() as u64;
|
||||
let shared = PostValidationShared::from_run_output(out);
|
||||
|
||||
if let Some((_out_dir, t)) = timing.as_ref() {
|
||||
t.record_count("instances_processed", out.tree.instances_processed as u64);
|
||||
t.record_count("instances_failed", out.tree.instances_failed as u64);
|
||||
t.record_count("instances_processed", shared.instances_processed as u64);
|
||||
t.record_count("instances_failed", shared.instances_failed as u64);
|
||||
}
|
||||
|
||||
let publication_points = out.publication_points.len();
|
||||
let publication_point_repo_sync_ms_total: u64 = out
|
||||
let publication_points = shared.publication_points.len();
|
||||
let publication_point_repo_sync_ms_total: u64 = shared
|
||||
.publication_points
|
||||
.iter()
|
||||
.map(|pp| pp.repo_sync_duration_ms.unwrap_or(0))
|
||||
.sum();
|
||||
let download_event_count = out.download_stats.events_total;
|
||||
let download_event_count = shared.download_stats.events_total;
|
||||
let rrdp_download_ms_total: u64 = ["rrdp_notification", "rrdp_snapshot", "rrdp_delta"]
|
||||
.iter()
|
||||
.map(|key| {
|
||||
out.download_stats
|
||||
shared
|
||||
.download_stats
|
||||
.by_kind
|
||||
.get(*key)
|
||||
.map(|item| item.duration_ms_total)
|
||||
.unwrap_or(0)
|
||||
})
|
||||
.sum();
|
||||
let rsync_download_ms_total = out
|
||||
let rsync_download_ms_total = shared
|
||||
.download_stats
|
||||
.by_kind
|
||||
.get("rsync")
|
||||
.map(|item| item.duration_ms_total)
|
||||
.unwrap_or(0);
|
||||
let repo_sync_ms_total = rrdp_download_ms_total + rsync_download_ms_total;
|
||||
let download_bytes_total: u64 = out
|
||||
let download_bytes_total: u64 = shared
|
||||
.download_stats
|
||||
.by_kind
|
||||
.values()
|
||||
@ -1517,51 +1738,66 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
||||
None
|
||||
};
|
||||
|
||||
let mut ccr_build_ms = None;
|
||||
let mut ccr_write_ms = None;
|
||||
if let Some(path) = args.ccr_out_path.as_deref() {
|
||||
let started = std::time::Instant::now();
|
||||
let trust_anchors = if out.discoveries.is_empty() {
|
||||
vec![out.discovery.trust_anchor.clone()]
|
||||
} else {
|
||||
out.discoveries
|
||||
.iter()
|
||||
.map(|item| item.trust_anchor.clone())
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let ccr = build_ccr_from_run(
|
||||
store.as_ref(),
|
||||
&trust_anchors,
|
||||
&out.tree.vrps,
|
||||
&out.tree.aspas,
|
||||
&out.tree.router_keys,
|
||||
time::OffsetDateTime::now_utc(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
ccr_build_ms = Some(started.elapsed().as_millis() as u64);
|
||||
let started = std::time::Instant::now();
|
||||
write_ccr_file(path, &ccr).map_err(|e| e.to_string())?;
|
||||
ccr_write_ms = Some(started.elapsed().as_millis() as u64);
|
||||
eprintln!("wrote CCR: {}", path.display());
|
||||
}
|
||||
let report_json_format = if args.report_json_compact {
|
||||
ReportJsonFormat::Compact
|
||||
} else {
|
||||
ReportJsonFormat::Pretty
|
||||
};
|
||||
let ccr_produced_at = time::OffsetDateTime::now_utc();
|
||||
let (report_result, ccr_result) = std::thread::scope(|scope| {
|
||||
let report_handle = scope.spawn(|| {
|
||||
run_report_task(
|
||||
&policy,
|
||||
validation_time,
|
||||
&shared,
|
||||
args.report_json_path.as_deref(),
|
||||
report_json_format,
|
||||
)
|
||||
});
|
||||
let ccr_handle = scope.spawn(|| {
|
||||
run_ccr_task(
|
||||
store.as_ref(),
|
||||
&shared,
|
||||
args.ccr_out_path.as_deref(),
|
||||
ccr_produced_at,
|
||||
)
|
||||
});
|
||||
let report_result = report_handle
|
||||
.join()
|
||||
.map_err(|_| "report task panicked".to_string())
|
||||
.and_then(|result| result);
|
||||
let ccr_result = ccr_handle
|
||||
.join()
|
||||
.map_err(|_| "ccr task panicked".to_string())
|
||||
.and_then(|result| result);
|
||||
(report_result, ccr_result)
|
||||
});
|
||||
let report_output = report_result?;
|
||||
let ccr_output = ccr_result?;
|
||||
let report = report_output.report;
|
||||
let report_build_ms = report_output.report_build_ms;
|
||||
let report_write_ms = report_output.report_write_ms;
|
||||
let ccr_build_ms = ccr_output.ccr_build_ms;
|
||||
let ccr_build_breakdown = ccr_output.ccr_build_breakdown;
|
||||
let ccr_write_ms = ccr_output.ccr_write_ms;
|
||||
|
||||
let mut cir_build_cir_ms = None;
|
||||
let mut cir_write_cir_ms = None;
|
||||
let mut cir_total_ms = None;
|
||||
if args.cir_enabled {
|
||||
let cir_tal_uris = resolve_cir_export_tal_uris(&args)?;
|
||||
if cir_tal_uris.len() != out.discoveries.len() {
|
||||
if cir_tal_uris.len() != shared.discoveries.len() {
|
||||
return Err(format!(
|
||||
"CIR export TAL URI count ({}) does not match discovery count ({})",
|
||||
cir_tal_uris.len(),
|
||||
out.discoveries.len()
|
||||
shared.discoveries.len()
|
||||
));
|
||||
}
|
||||
let cir_out_path = args
|
||||
.cir_out_path
|
||||
.as_deref()
|
||||
.expect("validated by parse_args for cir");
|
||||
let tal_bindings = out
|
||||
let tal_bindings = shared
|
||||
.discoveries
|
||||
.iter()
|
||||
.zip(cir_tal_uris.iter())
|
||||
@ -1574,14 +1810,10 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
||||
store.as_ref(),
|
||||
&tal_bindings,
|
||||
validation_time,
|
||||
&out.publication_points,
|
||||
shared.publication_points.as_ref(),
|
||||
cir_out_path,
|
||||
time::OffsetDateTime::now_utc().date(),
|
||||
if out.current_repo_objects.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(out.current_repo_objects.as_slice())
|
||||
},
|
||||
shared.current_repo_objects(),
|
||||
)
|
||||
.map_err(|e| e.to_string())?;
|
||||
cir_build_cir_ms = Some(summary.timing.build_cir_ms);
|
||||
@ -1597,49 +1829,26 @@ pub fn run(argv: &[String]) -> Result<(), String> {
|
||||
summary.timing.total_ms
|
||||
);
|
||||
}
|
||||
|
||||
let report_started = std::time::Instant::now();
|
||||
let report = build_report(&policy, validation_time, out);
|
||||
let report_build_ms = report_started.elapsed().as_millis() as u64;
|
||||
|
||||
let mut report_write_ms = None;
|
||||
if let Some(p) = args.report_json_path.as_deref() {
|
||||
let started = std::time::Instant::now();
|
||||
write_json(p, &report)?;
|
||||
report_write_ms = Some(started.elapsed().as_millis() as u64);
|
||||
if let Some(parent) = p.parent() {
|
||||
let stage_timing = RunStageTiming {
|
||||
validation_ms,
|
||||
report_build_ms,
|
||||
report_write_ms,
|
||||
ccr_build_ms,
|
||||
ccr_write_ms,
|
||||
cir_build_cir_ms,
|
||||
cir_write_cir_ms,
|
||||
cir_total_ms,
|
||||
total_ms: total_started.elapsed().as_millis() as u64,
|
||||
publication_points,
|
||||
repo_sync_ms_total,
|
||||
publication_point_repo_sync_ms_total,
|
||||
download_event_count,
|
||||
rrdp_download_ms_total,
|
||||
rsync_download_ms_total,
|
||||
download_bytes_total,
|
||||
};
|
||||
let stage_timing_path = parent.join("stage-timing.json");
|
||||
std::fs::write(
|
||||
&stage_timing_path,
|
||||
serde_json::to_vec_pretty(&stage_timing).map_err(|e| e.to_string())?,
|
||||
)
|
||||
.map_err(|e| {
|
||||
format!(
|
||||
"write stage timing failed: {}: {e}",
|
||||
stage_timing_path.display()
|
||||
)
|
||||
})?;
|
||||
eprintln!("analysis: wrote {}", stage_timing_path.display());
|
||||
}
|
||||
}
|
||||
let stage_timing = RunStageTiming {
|
||||
validation_ms,
|
||||
report_build_ms,
|
||||
report_write_ms,
|
||||
ccr_build_ms,
|
||||
ccr_build_breakdown,
|
||||
ccr_write_ms,
|
||||
cir_build_cir_ms,
|
||||
cir_write_cir_ms,
|
||||
cir_total_ms,
|
||||
total_ms: total_started.elapsed().as_millis() as u64,
|
||||
publication_points,
|
||||
repo_sync_ms_total,
|
||||
publication_point_repo_sync_ms_total,
|
||||
download_event_count,
|
||||
rrdp_download_ms_total,
|
||||
rsync_download_ms_total,
|
||||
download_bytes_total,
|
||||
};
|
||||
write_stage_timing(args.report_json_path.as_deref(), &stage_timing)?;
|
||||
|
||||
if let Some((out_dir, t)) = timing.as_ref() {
|
||||
t.record_count("vrps", report.vrps.len() as u64);
|
||||
@ -1773,6 +1982,43 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_accepts_report_json_compact_when_report_json_is_set() {
|
||||
let argv = vec![
|
||||
"rpki".to_string(),
|
||||
"--db".to_string(),
|
||||
"db".to_string(),
|
||||
"--tal-url".to_string(),
|
||||
"https://example.test/x.tal".to_string(),
|
||||
"--report-json".to_string(),
|
||||
"out/report.json".to_string(),
|
||||
"--report-json-compact".to_string(),
|
||||
];
|
||||
let args = parse_args(&argv).expect("parse args");
|
||||
assert_eq!(
|
||||
args.report_json_path.as_deref(),
|
||||
Some(std::path::Path::new("out/report.json"))
|
||||
);
|
||||
assert!(args.report_json_compact);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_rejects_report_json_compact_without_report_json() {
|
||||
let argv = vec![
|
||||
"rpki".to_string(),
|
||||
"--db".to_string(),
|
||||
"db".to_string(),
|
||||
"--tal-url".to_string(),
|
||||
"https://example.test/x.tal".to_string(),
|
||||
"--report-json-compact".to_string(),
|
||||
];
|
||||
let err = parse_args(&argv).expect_err("compact flag without report path should fail");
|
||||
assert!(
|
||||
err.contains("--report-json-compact requires --report-json"),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_accepts_external_raw_store_db() {
|
||||
let argv = vec![
|
||||
@ -2485,8 +2731,7 @@ mod tests {
|
||||
assert!(err.contains("read policy file failed"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_report_and_helpers_work_on_synthetic_output() {
|
||||
fn synthetic_post_validation_shared() -> PostValidationShared {
|
||||
let tal_bytes = std::fs::read(
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/tal/apnic-rfc7730-https.tal"),
|
||||
@ -2506,11 +2751,9 @@ mod tests {
|
||||
let tree = crate::validation::tree::TreeRunOutput {
|
||||
instances_processed: 1,
|
||||
instances_failed: 0,
|
||||
warnings: vec![
|
||||
crate::report::Warning::new("synthetic warning")
|
||||
.with_rfc_refs(&[crate::report::RfcRef("RFC 6487 §4.8.8.1")])
|
||||
.with_context("rsync://example.test/repo/pp/"),
|
||||
],
|
||||
warnings: vec![crate::report::Warning::new("synthetic warning")
|
||||
.with_rfc_refs(&[crate::report::RfcRef("RFC 6487 §4.8.8.1")])
|
||||
.with_context("rsync://example.test/repo/pp/")],
|
||||
vrps: vec![crate::validation::objects::Vrp {
|
||||
asn: 64496,
|
||||
prefix: crate::data_model::roa::IpPrefix {
|
||||
@ -2528,10 +2771,13 @@ mod tests {
|
||||
};
|
||||
|
||||
let mut pp1 = crate::audit::PublicationPointAudit::default();
|
||||
pp1.source = "fresh".to_string();
|
||||
pp1.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
|
||||
let mut pp2 = crate::audit::PublicationPointAudit::default();
|
||||
pp2.source = "fresh".to_string();
|
||||
pp2.rrdp_notification_uri = Some("https://example.test/n1.xml".to_string());
|
||||
let mut pp3 = crate::audit::PublicationPointAudit::default();
|
||||
pp3.source = "fresh".to_string();
|
||||
pp3.rrdp_notification_uri = Some("https://example.test/n2.xml".to_string());
|
||||
|
||||
let out = crate::validation::run_tree_from_tal::RunTreeFromTalAuditOutput {
|
||||
@ -2542,11 +2788,54 @@ mod tests {
|
||||
downloads: Vec::new(),
|
||||
download_stats: crate::audit::AuditDownloadStats::default(),
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
PostValidationShared::from_run_output(out)
|
||||
}
|
||||
|
||||
fn sample_cli_ccr_accumulator() -> CcrAccumulator {
|
||||
let tal_bytes = std::fs::read(
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/tal/apnic-rfc7730-https.tal"),
|
||||
)
|
||||
.expect("read tal fixture");
|
||||
let ta_der = std::fs::read(
|
||||
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("tests/fixtures/ta/apnic-ta.cer"),
|
||||
)
|
||||
.expect("read ta fixture");
|
||||
let discovery = crate::validation::from_tal::discover_root_ca_instance_from_tal_and_ta_der(
|
||||
&tal_bytes, &ta_der, None,
|
||||
)
|
||||
.expect("discover root");
|
||||
let mut accumulator = CcrAccumulator::new(vec![discovery.trust_anchor.clone()]);
|
||||
let projection = crate::storage::VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
||||
manifest_sha256: vec![0x44; 32],
|
||||
manifest_size: 2048,
|
||||
manifest_ee_aki: vec![0x55; 20],
|
||||
manifest_number_be: vec![1],
|
||||
manifest_this_update: crate::storage::PackTime::from_utc_offset_datetime(
|
||||
time::OffsetDateTime::now_utc(),
|
||||
),
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis: vec![vec![0x33; 20]],
|
||||
};
|
||||
accumulator
|
||||
.append_manifest_projection(&projection)
|
||||
.expect("append manifest projection");
|
||||
accumulator
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_report_and_helpers_work_on_synthetic_output() {
|
||||
let shared = synthetic_post_validation_shared();
|
||||
let policy = Policy::default();
|
||||
let validation_time = time::OffsetDateTime::now_utc();
|
||||
let report = build_report(&policy, validation_time, out);
|
||||
let report = build_report(&policy, validation_time, &shared);
|
||||
|
||||
assert_eq!(unique_rrdp_repos(&report), 2);
|
||||
assert_eq!(report.vrps.len(), 1);
|
||||
@ -2555,6 +2844,92 @@ mod tests {
|
||||
print_summary(&report);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_report_task_and_stage_timing_work() {
|
||||
let shared = synthetic_post_validation_shared();
|
||||
let policy = Policy::default();
|
||||
let validation_time = time::OffsetDateTime::now_utc();
|
||||
let dir = tempfile::tempdir().expect("tmpdir");
|
||||
let report_path = dir.path().join("report.json");
|
||||
let report_output = run_report_task(
|
||||
&policy,
|
||||
validation_time,
|
||||
&shared,
|
||||
Some(&report_path),
|
||||
ReportJsonFormat::Compact,
|
||||
)
|
||||
.expect("run report task");
|
||||
|
||||
assert_eq!(report_output.report.vrps.len(), 1);
|
||||
assert_eq!(report_output.report.aspas.len(), 1);
|
||||
assert!(report_output.report_write_ms.is_some());
|
||||
|
||||
let report_json = std::fs::read_to_string(&report_path).expect("read report json");
|
||||
assert!(!report_json.contains('\n'), "{report_json}");
|
||||
|
||||
let stage_timing = RunStageTiming {
|
||||
validation_ms: 1,
|
||||
report_build_ms: report_output.report_build_ms,
|
||||
report_write_ms: report_output.report_write_ms,
|
||||
ccr_build_ms: Some(2),
|
||||
ccr_build_breakdown: None,
|
||||
ccr_write_ms: Some(3),
|
||||
cir_build_cir_ms: Some(4),
|
||||
cir_write_cir_ms: Some(5),
|
||||
cir_total_ms: Some(6),
|
||||
total_ms: 7,
|
||||
publication_points: shared.publication_points.len(),
|
||||
repo_sync_ms_total: 8,
|
||||
publication_point_repo_sync_ms_total: 9,
|
||||
download_event_count: 10,
|
||||
rrdp_download_ms_total: 11,
|
||||
rsync_download_ms_total: 12,
|
||||
download_bytes_total: 13,
|
||||
};
|
||||
write_stage_timing(Some(&report_path), &stage_timing).expect("write stage timing");
|
||||
let stage_timing_json =
|
||||
std::fs::read_to_string(dir.path().join("stage-timing.json")).expect("read timing");
|
||||
assert!(stage_timing_json.contains("\"validation_ms\""));
|
||||
assert!(stage_timing_json.contains("\"ccr_build_ms\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_ccr_task_uses_accumulator_when_phase2_output_contains_reuse_sources() {
|
||||
let mut shared = synthetic_post_validation_shared();
|
||||
shared.ccr_accumulator = Some(sample_cli_ccr_accumulator());
|
||||
let mut publication_points = shared
|
||||
.publication_points
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
publication_points[1].source = "vcir_current_instance".to_string();
|
||||
publication_points[2].source = "failed_no_cache".to_string();
|
||||
shared.publication_points = publication_points.into();
|
||||
let dir = tempfile::tempdir().expect("tmpdir");
|
||||
let ccr_path = dir.path().join("result.ccr");
|
||||
let store = RocksStore::open(&dir.path().join("db")).expect("open empty store");
|
||||
|
||||
let output = run_ccr_task(
|
||||
&store,
|
||||
&shared,
|
||||
Some(&ccr_path),
|
||||
time::OffsetDateTime::now_utc(),
|
||||
)
|
||||
.expect("run ccr task");
|
||||
|
||||
assert!(output.ccr_build_ms.is_some());
|
||||
assert!(output.ccr_build_breakdown.is_none());
|
||||
let der = std::fs::read(&ccr_path).expect("read ccr");
|
||||
let ci = crate::ccr::decode_content_info(&der).expect("decode ccr");
|
||||
assert_eq!(
|
||||
ci.content
|
||||
.mfts
|
||||
.as_ref()
|
||||
.map(|manifest_state| manifest_state.mis.len()),
|
||||
Some(1)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_json_writes_report() {
|
||||
let report = AuditReportV2 {
|
||||
@ -2577,11 +2952,19 @@ mod tests {
|
||||
};
|
||||
|
||||
let dir = tempfile::tempdir().expect("tmpdir");
|
||||
let p = dir.path().join("report.json");
|
||||
write_json(&p, &report).expect("write json");
|
||||
let s = std::fs::read_to_string(&p).expect("read report");
|
||||
assert!(s.contains("\"format_version\""));
|
||||
assert!(s.contains("\"policy\""));
|
||||
let pretty_path = dir.path().join("report-pretty.json");
|
||||
write_json(&pretty_path, &report, ReportJsonFormat::Pretty).expect("write pretty json");
|
||||
let pretty = std::fs::read_to_string(&pretty_path).expect("read pretty report");
|
||||
assert!(pretty.contains("\"format_version\""));
|
||||
assert!(pretty.contains("\"policy\""));
|
||||
assert!(pretty.contains("\n \"format_version\""), "{pretty}");
|
||||
|
||||
let compact_path = dir.path().join("report-compact.json");
|
||||
write_json(&compact_path, &report, ReportJsonFormat::Compact).expect("write compact json");
|
||||
let compact = std::fs::read_to_string(&compact_path).expect("read compact report");
|
||||
assert!(compact.contains("\"format_version\""));
|
||||
assert!(compact.contains("\"policy\""));
|
||||
assert!(!compact.contains('\n'), "{compact}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
357
src/storage.rs
357
src/storage.rs
@ -2,13 +2,14 @@ use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
use rocksdb::{
|
||||
ColumnFamily, ColumnFamilyDescriptor, DB, DBCompressionType, Direction, IteratorMode, Options,
|
||||
WriteBatch,
|
||||
ColumnFamily, ColumnFamilyDescriptor, DBCompressionType, Direction, IteratorMode, Options,
|
||||
WriteBatch, DB,
|
||||
};
|
||||
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use sha2::Digest;
|
||||
|
||||
use crate::blob_store::{ExternalRawStoreDb, RawObjectStore};
|
||||
use crate::data_model::common::der_take_tlv;
|
||||
use crate::data_model::rc::{AsResourceSet, IpResourceSet};
|
||||
|
||||
pub const CF_REPOSITORY_VIEW: &str = "repository_view";
|
||||
@ -231,6 +232,69 @@ impl ValidatedManifestMeta {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VcirCcrManifestProjection {
|
||||
pub manifest_rsync_uri: String,
|
||||
pub manifest_sha256: Vec<u8>,
|
||||
pub manifest_size: u64,
|
||||
pub manifest_ee_aki: Vec<u8>,
|
||||
pub manifest_number_be: Vec<u8>,
|
||||
pub manifest_this_update: PackTime,
|
||||
pub manifest_sia_locations_der: Vec<Vec<u8>>,
|
||||
pub subordinate_skis: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl VcirCcrManifestProjection {
|
||||
pub fn validate_internal(&self) -> StorageResult<()> {
|
||||
validate_non_empty(
|
||||
"vcir.ccr_manifest_projection.manifest_rsync_uri",
|
||||
&self.manifest_rsync_uri,
|
||||
)?;
|
||||
validate_sha256_digest_bytes(
|
||||
"vcir.ccr_manifest_projection.manifest_sha256",
|
||||
&self.manifest_sha256,
|
||||
)?;
|
||||
if self.manifest_size < 1000 {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: "vcir.ccr_manifest_projection.manifest_size",
|
||||
detail: format!("must be >= 1000, got {}", self.manifest_size),
|
||||
});
|
||||
}
|
||||
validate_fixed_len_bytes(
|
||||
"vcir.ccr_manifest_projection.manifest_ee_aki",
|
||||
&self.manifest_ee_aki,
|
||||
20,
|
||||
)?;
|
||||
validate_manifest_number_be(
|
||||
"vcir.ccr_manifest_projection.manifest_number_be",
|
||||
&self.manifest_number_be,
|
||||
)?;
|
||||
parse_time(
|
||||
"vcir.ccr_manifest_projection.manifest_this_update",
|
||||
&self.manifest_this_update,
|
||||
)?;
|
||||
if self.manifest_sia_locations_der.is_empty() {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: "vcir.ccr_manifest_projection.manifest_sia_locations_der",
|
||||
detail: "must contain at least one AccessDescription".to_string(),
|
||||
});
|
||||
}
|
||||
for location in &self.manifest_sia_locations_der {
|
||||
validate_full_der_with_tag(
|
||||
"vcir.ccr_manifest_projection.manifest_sia_locations_der[]",
|
||||
location,
|
||||
Some(0x30),
|
||||
)?;
|
||||
}
|
||||
validate_sorted_unique_fixed_len_bytes(
|
||||
"vcir.ccr_manifest_projection.subordinate_skis",
|
||||
&self.subordinate_skis,
|
||||
20,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct VcirInstanceGate {
|
||||
pub manifest_next_update: PackTime,
|
||||
@ -469,6 +533,7 @@ pub struct ValidatedCaInstanceResult {
|
||||
pub current_manifest_rsync_uri: String,
|
||||
pub current_crl_rsync_uri: String,
|
||||
pub validated_manifest_meta: ValidatedManifestMeta,
|
||||
pub ccr_manifest_projection: VcirCcrManifestProjection,
|
||||
pub instance_gate: VcirInstanceGate,
|
||||
pub child_entries: Vec<VcirChildEntry>,
|
||||
pub local_outputs: Vec<VcirLocalOutput>,
|
||||
@ -497,6 +562,7 @@ impl ValidatedCaInstanceResult {
|
||||
)?;
|
||||
validate_non_empty("vcir.current_crl_rsync_uri", &self.current_crl_rsync_uri)?;
|
||||
self.validated_manifest_meta.validate_internal()?;
|
||||
self.ccr_manifest_projection.validate_internal()?;
|
||||
self.instance_gate.validate_internal()?;
|
||||
|
||||
let expected_manifest_next = self
|
||||
@ -1554,6 +1620,75 @@ fn validate_manifest_number_be(field: &'static str, value: &[u8]) -> StorageResu
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_sha256_digest_bytes(field: &'static str, value: &[u8]) -> StorageResult<()> {
|
||||
if value.len() != 32 {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: field,
|
||||
detail: format!("must be 32 bytes, got {}", value.len()),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_fixed_len_bytes(
|
||||
field: &'static str,
|
||||
value: &[u8],
|
||||
expected_len: usize,
|
||||
) -> StorageResult<()> {
|
||||
if value.len() != expected_len {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: field,
|
||||
detail: format!("must be {expected_len} bytes, got {}", value.len()),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_sorted_unique_fixed_len_bytes(
|
||||
field: &'static str,
|
||||
values: &[Vec<u8>],
|
||||
expected_len: usize,
|
||||
) -> StorageResult<()> {
|
||||
for value in values {
|
||||
validate_fixed_len_bytes(field, value, expected_len)?;
|
||||
}
|
||||
for window in values.windows(2) {
|
||||
if window[0] >= window[1] {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: field,
|
||||
detail: "must be strictly sorted and unique".to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_full_der_with_tag(
|
||||
field: &'static str,
|
||||
der: &[u8],
|
||||
expected_tag: Option<u8>,
|
||||
) -> StorageResult<()> {
|
||||
let (tag, _value, rem) = der_take_tlv(der).map_err(|detail| StorageError::InvalidData {
|
||||
entity: field,
|
||||
detail,
|
||||
})?;
|
||||
if !rem.is_empty() {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: field,
|
||||
detail: "trailing bytes after DER object".to_string(),
|
||||
});
|
||||
}
|
||||
if let Some(expected_tag) = expected_tag {
|
||||
if tag != expected_tag {
|
||||
return Err(StorageError::InvalidData {
|
||||
entity: field,
|
||||
detail: format!("unexpected tag 0x{tag:02X}, expected 0x{expected_tag:02X}"),
|
||||
});
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_time(field: &'static str, value: &PackTime) -> StorageResult<time::OffsetDateTime> {
|
||||
value.parse().map_err(|detail| StorageError::InvalidData {
|
||||
entity: field,
|
||||
@ -1761,10 +1896,31 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_ccr_manifest_projection(
|
||||
manifest_rsync_uri: &str,
|
||||
manifest_this_update: PackTime,
|
||||
subordinate_skis: Vec<Vec<u8>>,
|
||||
) -> VcirCcrManifestProjection {
|
||||
VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
manifest_sha256: vec![0x11; 32],
|
||||
manifest_size: 4096,
|
||||
manifest_ee_aki: vec![0x22; 20],
|
||||
manifest_number_be: vec![3],
|
||||
manifest_this_update,
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis,
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_vcir(manifest_rsync_uri: &str) -> ValidatedCaInstanceResult {
|
||||
let roa_bytes = b"roa-object".to_vec();
|
||||
let ee_bytes = b"ee-cert".to_vec();
|
||||
let child_bytes = b"child-cert".to_vec();
|
||||
let child_ski = "1234567890abcdef1234567890abcdef12345678".to_string();
|
||||
ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
parent_manifest_rsync_uri: Some(
|
||||
@ -1782,6 +1938,11 @@ mod tests {
|
||||
validated_manifest_this_update: pack_time(0),
|
||||
validated_manifest_next_update: pack_time(24),
|
||||
},
|
||||
ccr_manifest_projection: sample_ccr_manifest_projection(
|
||||
manifest_rsync_uri,
|
||||
pack_time(0),
|
||||
vec![hex::decode(&child_ski).expect("decode child ski")],
|
||||
),
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: pack_time(24),
|
||||
current_crl_next_update: pack_time(12),
|
||||
@ -1792,7 +1953,7 @@ mod tests {
|
||||
child_manifest_rsync_uri: "rsync://example.test/repo/child/child.mft".to_string(),
|
||||
child_cert_rsync_uri: "rsync://example.test/repo/child/child.cer".to_string(),
|
||||
child_cert_hash: sha256_hex(&child_bytes),
|
||||
child_ski: "1234567890abcdef1234567890abcdef12345678".to_string(),
|
||||
child_ski,
|
||||
child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(),
|
||||
child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(),
|
||||
child_rrdp_notification_uri: Some(
|
||||
@ -1863,6 +2024,51 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vcir_ccr_manifest_projection_validate_accepts_valid_projection() {
|
||||
let projection = sample_ccr_manifest_projection(
|
||||
"rsync://example.test/repo/current.mft",
|
||||
pack_time(0),
|
||||
vec![vec![0x33; 20], vec![0x44; 20]],
|
||||
);
|
||||
projection.validate_internal().expect("valid projection");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vcir_ccr_manifest_projection_validate_rejects_invalid_fields() {
|
||||
let mut bad_hash = sample_ccr_manifest_projection(
|
||||
"rsync://example.test/repo/current.mft",
|
||||
pack_time(0),
|
||||
vec![vec![0x33; 20]],
|
||||
);
|
||||
bad_hash.manifest_sha256 = vec![0x11; 31];
|
||||
assert!(matches!(
|
||||
bad_hash.validate_internal(),
|
||||
Err(StorageError::InvalidData { .. })
|
||||
));
|
||||
|
||||
let mut bad_locations = sample_ccr_manifest_projection(
|
||||
"rsync://example.test/repo/current.mft",
|
||||
pack_time(0),
|
||||
vec![vec![0x33; 20]],
|
||||
);
|
||||
bad_locations.manifest_sia_locations_der = vec![vec![0x04, 0x00]];
|
||||
assert!(matches!(
|
||||
bad_locations.validate_internal(),
|
||||
Err(StorageError::InvalidData { .. })
|
||||
));
|
||||
|
||||
let bad_subordinates = sample_ccr_manifest_projection(
|
||||
"rsync://example.test/repo/current.mft",
|
||||
pack_time(0),
|
||||
vec![vec![0x44; 20], vec![0x33; 20]],
|
||||
);
|
||||
assert!(matches!(
|
||||
bad_subordinates.validate_internal(),
|
||||
Err(StorageError::InvalidData { .. })
|
||||
));
|
||||
}
|
||||
|
||||
fn sample_audit_rule_entry(kind: AuditRuleKind) -> AuditRuleIndexEntry {
|
||||
AuditRuleIndexEntry {
|
||||
kind,
|
||||
@ -1964,12 +2170,10 @@ mod tests {
|
||||
store
|
||||
.delete_repository_view_entry(&entry1.rsync_uri)
|
||||
.expect("delete repository view entry1");
|
||||
assert!(
|
||||
store
|
||||
.get_repository_view_entry(&entry1.rsync_uri)
|
||||
.expect("get deleted repository view entry1")
|
||||
.is_none()
|
||||
);
|
||||
assert!(store
|
||||
.get_repository_view_entry(&entry1.rsync_uri)
|
||||
.expect("get deleted repository view entry1")
|
||||
.is_none());
|
||||
|
||||
let raw = sample_raw_by_hash_entry(b"raw-der-object".to_vec());
|
||||
store
|
||||
@ -2026,12 +2230,10 @@ mod tests {
|
||||
store.get_blob_bytes(&hash).expect("get blob bytes"),
|
||||
Some(bytes.clone())
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.get_raw_by_hash_entry(&hash)
|
||||
.expect("get raw entry")
|
||||
.is_none()
|
||||
);
|
||||
assert!(store
|
||||
.get_raw_by_hash_entry(&hash)
|
||||
.expect("get raw entry")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2100,10 +2302,7 @@ mod tests {
|
||||
let batch = store
|
||||
.get_blob_bytes_batch(&[blob_hash.clone(), raw.sha256_hex.clone(), "00".repeat(32)])
|
||||
.expect("get blob bytes batch");
|
||||
assert_eq!(
|
||||
batch,
|
||||
vec![Some(blob_bytes), Some(raw.bytes.clone()), None]
|
||||
);
|
||||
assert_eq!(batch, vec![Some(blob_bytes), Some(raw.bytes.clone()), None]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2156,12 +2355,10 @@ mod tests {
|
||||
let td = tempfile::tempdir().expect("tempdir");
|
||||
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
||||
|
||||
assert!(
|
||||
store
|
||||
.get_blob_bytes_batch(&[])
|
||||
.expect("empty blob batch request")
|
||||
.is_empty()
|
||||
);
|
||||
assert!(store
|
||||
.get_blob_bytes_batch(&[])
|
||||
.expect("empty blob batch request")
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2232,7 +2429,10 @@ mod tests {
|
||||
.delete_raw_by_hash_entry(&raw.sha256_hex)
|
||||
.expect("delete external raw entry");
|
||||
|
||||
assert!(store.get_raw_by_hash_entry(&raw.sha256_hex).unwrap().is_none());
|
||||
assert!(store
|
||||
.get_raw_by_hash_entry(&raw.sha256_hex)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(store.get_blob_bytes(&raw.sha256_hex).unwrap().is_none());
|
||||
}
|
||||
|
||||
@ -2296,12 +2496,10 @@ mod tests {
|
||||
store
|
||||
.delete_vcir(&vcir.manifest_rsync_uri)
|
||||
.expect("delete vcir");
|
||||
assert!(
|
||||
store
|
||||
.get_vcir(&vcir.manifest_rsync_uri)
|
||||
.expect("get deleted vcir")
|
||||
.is_none()
|
||||
);
|
||||
assert!(store
|
||||
.get_vcir(&vcir.manifest_rsync_uri)
|
||||
.expect("get deleted vcir")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2356,12 +2554,10 @@ mod tests {
|
||||
store
|
||||
.delete_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
|
||||
.expect("delete roa audit rule entry");
|
||||
assert!(
|
||||
store
|
||||
.get_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
|
||||
.expect("get deleted roa audit rule entry")
|
||||
.is_none()
|
||||
);
|
||||
assert!(store
|
||||
.get_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
|
||||
.expect("get deleted roa audit rule entry")
|
||||
.is_none());
|
||||
|
||||
let mut invalid = sample_audit_rule_entry(AuditRuleKind::Roa);
|
||||
invalid.rule_hash = "bad".to_string();
|
||||
@ -2395,15 +2591,10 @@ mod tests {
|
||||
store
|
||||
.replace_vcir_and_audit_rule_indexes(None, &previous)
|
||||
.expect("store previous vcir");
|
||||
assert!(
|
||||
store
|
||||
.get_audit_rule_index_entry(
|
||||
AuditRuleKind::Roa,
|
||||
&previous.local_outputs[0].rule_hash
|
||||
)
|
||||
.expect("get old audit entry")
|
||||
.is_some()
|
||||
);
|
||||
assert!(store
|
||||
.get_audit_rule_index_entry(AuditRuleKind::Roa, &previous.local_outputs[0].rule_hash)
|
||||
.expect("get old audit entry")
|
||||
.is_some());
|
||||
|
||||
let mut current = sample_vcir("rsync://example.test/repo/current.mft");
|
||||
current.local_outputs = vec![VcirLocalOutput {
|
||||
@ -2429,24 +2620,14 @@ mod tests {
|
||||
.expect("get replaced vcir")
|
||||
.expect("vcir exists");
|
||||
assert_eq!(got, current);
|
||||
assert!(
|
||||
store
|
||||
.get_audit_rule_index_entry(
|
||||
AuditRuleKind::Roa,
|
||||
&previous.local_outputs[0].rule_hash
|
||||
)
|
||||
.expect("get deleted old audit entry")
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
store
|
||||
.get_audit_rule_index_entry(
|
||||
AuditRuleKind::Aspa,
|
||||
¤t.local_outputs[0].rule_hash
|
||||
)
|
||||
.expect("get new audit entry")
|
||||
.is_some()
|
||||
);
|
||||
assert!(store
|
||||
.get_audit_rule_index_entry(AuditRuleKind::Roa, &previous.local_outputs[0].rule_hash)
|
||||
.expect("get deleted old audit entry")
|
||||
.is_none());
|
||||
assert!(store
|
||||
.get_audit_rule_index_entry(AuditRuleKind::Aspa, ¤t.local_outputs[0].rule_hash)
|
||||
.expect("get new audit entry")
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2588,12 +2769,10 @@ mod tests {
|
||||
store
|
||||
.delete_rrdp_uri_owner_record(&member1.rsync_uri)
|
||||
.expect("delete uri owner record");
|
||||
assert!(
|
||||
store
|
||||
.get_rrdp_uri_owner_record(&member1.rsync_uri)
|
||||
.expect("get deleted uri owner")
|
||||
.is_none()
|
||||
);
|
||||
assert!(store
|
||||
.get_rrdp_uri_owner_record(&member1.rsync_uri)
|
||||
.expect("get deleted uri owner")
|
||||
.is_none());
|
||||
|
||||
let mut invalid_source =
|
||||
sample_rrdp_source_record("https://invalid.example/notification.xml");
|
||||
@ -2724,21 +2903,15 @@ mod tests {
|
||||
]
|
||||
);
|
||||
|
||||
assert!(
|
||||
store
|
||||
.is_current_rrdp_source_member(notify_uri, &present_a.rsync_uri)
|
||||
.expect("current a")
|
||||
);
|
||||
assert!(
|
||||
!store
|
||||
.is_current_rrdp_source_member(notify_uri, &withdrawn_b.rsync_uri)
|
||||
.expect("withdrawn b")
|
||||
);
|
||||
assert!(
|
||||
!store
|
||||
.is_current_rrdp_source_member(notify_uri, &other_source.rsync_uri)
|
||||
.expect("other source")
|
||||
);
|
||||
assert!(store
|
||||
.is_current_rrdp_source_member(notify_uri, &present_a.rsync_uri)
|
||||
.expect("current a"));
|
||||
assert!(!store
|
||||
.is_current_rrdp_source_member(notify_uri, &withdrawn_b.rsync_uri)
|
||||
.expect("withdrawn b"));
|
||||
assert!(!store
|
||||
.is_current_rrdp_source_member(notify_uri, &other_source.rsync_uri)
|
||||
.expect("other source"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -2904,12 +3077,10 @@ mod tests {
|
||||
assert_eq!(got.current_hash_hex, hash);
|
||||
assert_eq!(got.current_hash, compute_sha256_32(&bytes));
|
||||
assert_eq!(got.bytes, bytes);
|
||||
assert!(
|
||||
store
|
||||
.get_raw_by_hash_entry(&got.current_hash_hex)
|
||||
.expect("get raw entry")
|
||||
.is_none()
|
||||
);
|
||||
assert!(store
|
||||
.get_raw_by_hash_entry(&got.current_hash_hex)
|
||||
.expect("get raw entry")
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@ -4,7 +4,7 @@ use crate::data_model::signed_object::SignedObjectVerifyError;
|
||||
use crate::policy::{CaFailedFetchPolicy, Policy};
|
||||
use crate::report::{RfcRef, Warning};
|
||||
use crate::storage::{PackFile, PackTime, RocksStore, StorageError, VcirArtifactRole};
|
||||
use crate::validation::cert_path::{CertPathError, validate_signed_object_ee_cert_path_fast};
|
||||
use crate::validation::cert_path::{validate_signed_object_ee_cert_path_fast, CertPathError};
|
||||
use crate::validation::publication_point::PublicationPointSnapshot;
|
||||
use sha2::Digest;
|
||||
use std::cmp::Ordering;
|
||||
@ -398,11 +398,10 @@ pub fn process_manifest_publication_point_after_repo_sync(
|
||||
Err(ManifestProcessError::StopAllOutput(fresh_err))
|
||||
}
|
||||
CaFailedFetchPolicy::ReuseCurrentInstanceVcir => {
|
||||
let mut warnings = vec![
|
||||
Warning::new(format!("manifest failed fetch: {fresh_err}"))
|
||||
let mut warnings =
|
||||
vec![Warning::new(format!("manifest failed fetch: {fresh_err}"))
|
||||
.with_rfc_refs(&[RfcRef("RFC 9286 §6.6")])
|
||||
.with_context(manifest_rsync_uri),
|
||||
];
|
||||
.with_context(manifest_rsync_uri)];
|
||||
|
||||
match load_current_instance_vcir_publication_point(
|
||||
store,
|
||||
@ -932,7 +931,8 @@ mod tests {
|
||||
use crate::storage::{
|
||||
PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult,
|
||||
ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole, VcirArtifactValidationStatus,
|
||||
VcirAuditSummary, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||
VcirAuditSummary, VcirCcrManifestProjection, VcirInstanceGate, VcirRelatedArtifact,
|
||||
VcirSummary,
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
@ -1039,6 +1039,19 @@ mod tests {
|
||||
) -> ValidatedCaInstanceResult {
|
||||
let gate_time =
|
||||
PackTime::from_utc_offset_datetime(validation_time + time::Duration::hours(1));
|
||||
let ccr_manifest_projection = VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
manifest_sha256: hex::decode(manifest_sha256).expect("decode manifest sha256"),
|
||||
manifest_size: 2048,
|
||||
manifest_ee_aki: vec![0x11; 20],
|
||||
manifest_number_be: vec![1],
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(validation_time),
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis: Vec::new(),
|
||||
};
|
||||
ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
@ -1054,6 +1067,7 @@ mod tests {
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(validation_time),
|
||||
validated_manifest_next_update: gate_time.clone(),
|
||||
},
|
||||
ccr_manifest_projection,
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: gate_time.clone(),
|
||||
current_crl_next_update: gate_time.clone(),
|
||||
@ -1313,12 +1327,10 @@ mod tests {
|
||||
.apply_repository_view_entries(&entries)
|
||||
.expect("apply current index");
|
||||
|
||||
assert!(
|
||||
store
|
||||
.get_repository_view_entry(&manifest_rsync_uri)
|
||||
.expect("get repository view")
|
||||
.is_none()
|
||||
);
|
||||
assert!(store
|
||||
.get_repository_view_entry(&manifest_rsync_uri)
|
||||
.expect("get repository view")
|
||||
.is_none());
|
||||
|
||||
let (fresh, _timing) = try_build_fresh_publication_point_with_timing(
|
||||
&store,
|
||||
|
||||
@ -77,6 +77,7 @@ pub fn run_publication_point_once(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let result = runner
|
||||
|
||||
@ -3,6 +3,7 @@ use url::Url;
|
||||
use crate::analysis::timing::TimingHandle;
|
||||
use crate::audit::PublicationPointAudit;
|
||||
use crate::audit_downloads::DownloadLogHandle;
|
||||
use crate::ccr::CcrAccumulator;
|
||||
use crate::current_repo_index::{CurrentRepoIndexHandle, CurrentRepoObject};
|
||||
use crate::data_model::ta::TrustAnchor;
|
||||
use crate::parallel::config::{ParallelPhase1Config, ParallelPhase2Config};
|
||||
@ -90,6 +91,7 @@ pub struct RunTreeFromTalAuditOutput {
|
||||
pub downloads: Vec<crate::audit::AuditDownloadEvent>,
|
||||
pub download_stats: crate::audit::AuditDownloadStats,
|
||||
pub current_repo_objects: Vec<CurrentRepoObject>,
|
||||
pub ccr_accumulator: Option<CcrAccumulator>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@ -118,6 +120,7 @@ fn make_live_runner<'a>(
|
||||
current_repo_index: Option<CurrentRepoIndexHandle>,
|
||||
repo_sync_runtime: Option<Arc<dyn RepoSyncRuntime>>,
|
||||
parallel_phase2_config: Option<ParallelPhase2Config>,
|
||||
ccr_accumulator: Option<CcrAccumulator>,
|
||||
) -> Rpkiv1PublicationPointRunner<'a> {
|
||||
let parallel_roa_worker_pool = parallel_phase2_config
|
||||
.as_ref()
|
||||
@ -140,6 +143,7 @@ fn make_live_runner<'a>(
|
||||
repo_sync_runtime,
|
||||
parallel_phase2_config,
|
||||
parallel_roa_worker_pool,
|
||||
ccr_accumulator: ccr_accumulator.map(Mutex::new),
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,6 +295,7 @@ pub fn run_tree_from_tal_url_serial(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -327,6 +332,7 @@ pub fn run_tree_from_tal_url_serial_audit(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -350,6 +356,7 @@ pub fn run_tree_from_tal_url_serial_audit(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -379,6 +386,7 @@ pub fn run_tree_from_tal_url_serial_audit_with_timing(
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -403,6 +411,7 @@ pub fn run_tree_from_tal_url_serial_audit_with_timing(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -446,6 +455,7 @@ where
|
||||
Some(current_repo_index),
|
||||
Some(runtime),
|
||||
phase2_config,
|
||||
phase2_enabled.then(|| CcrAccumulator::new(vec![discovery.trust_anchor.clone()])),
|
||||
);
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -472,6 +482,9 @@ where
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: snapshot_current_repo_objects(Some(¤t_repo_index_for_output)),
|
||||
ccr_accumulator: phase2_enabled
|
||||
.then(|| runner.ccr_accumulator_snapshot())
|
||||
.flatten(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -532,6 +545,14 @@ where
|
||||
Some(current_repo_index),
|
||||
Some(runtime),
|
||||
phase2_config,
|
||||
phase2_enabled.then(|| {
|
||||
CcrAccumulator::new(
|
||||
discoveries
|
||||
.iter()
|
||||
.map(|item| item.trust_anchor.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}),
|
||||
);
|
||||
|
||||
let TreeRunAuditOutput {
|
||||
@ -552,6 +573,9 @@ where
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: snapshot_current_repo_objects(Some(¤t_repo_index_for_output)),
|
||||
ccr_accumulator: phase2_enabled
|
||||
.then(|| runner.ccr_accumulator_snapshot())
|
||||
.flatten(),
|
||||
})
|
||||
}
|
||||
|
||||
@ -792,6 +816,7 @@ pub fn run_tree_from_tal_and_ta_der_serial(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -842,6 +867,7 @@ pub fn run_tree_from_tal_bytes_serial_audit(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -865,6 +891,7 @@ pub fn run_tree_from_tal_bytes_serial_audit(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -908,6 +935,7 @@ pub fn run_tree_from_tal_bytes_serial_audit_with_timing(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -933,6 +961,7 @@ pub fn run_tree_from_tal_bytes_serial_audit_with_timing(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -969,6 +998,7 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -992,6 +1022,7 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1031,6 +1062,7 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit_with_timing(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -1055,6 +1087,7 @@ pub fn run_tree_from_tal_and_ta_der_serial_audit_with_timing(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1100,6 +1133,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -1156,6 +1190,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -1179,6 +1214,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1228,6 +1264,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let root = root_handle_from_trust_anchor(
|
||||
@ -1252,6 +1289,7 @@ pub fn run_tree_from_tal_and_ta_der_payload_replay_serial_audit_with_timing(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1283,6 +1321,7 @@ fn build_payload_replay_runner<'a>(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1314,6 +1353,7 @@ fn build_payload_delta_replay_runner<'a>(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1345,6 +1385,7 @@ fn build_payload_delta_replay_current_store_runner<'a>(
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1461,6 +1502,7 @@ fn run_payload_delta_replay_audit_inner(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1605,6 +1647,7 @@ fn run_payload_delta_replay_step_audit_inner(
|
||||
downloads,
|
||||
download_stats,
|
||||
current_repo_objects: Vec::new(),
|
||||
ccr_accumulator: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -4,11 +4,12 @@ use crate::audit::{
|
||||
ObjectAuditEntry, PublicationPointAudit,
|
||||
};
|
||||
use crate::audit_downloads::DownloadLogHandle;
|
||||
use crate::ccr::CcrAccumulator;
|
||||
use crate::current_repo_index::CurrentRepoIndexHandle;
|
||||
use crate::data_model::aspa::AspaObject;
|
||||
use crate::data_model::crl::RpkixCrl;
|
||||
use crate::data_model::manifest::ManifestObject;
|
||||
use crate::data_model::rc::ResourceCertificate;
|
||||
use crate::data_model::rc::{AccessDescription, ResourceCertificate, SubjectInfoAccess};
|
||||
use crate::data_model::roa::{RoaAfi, RoaObject};
|
||||
use crate::data_model::router_cert::{
|
||||
BgpsecRouterCertificate, BgpsecRouterCertificateDecodeError, BgpsecRouterCertificatePathError,
|
||||
@ -23,8 +24,9 @@ use crate::replay::delta_archive::ReplayDeltaArchiveIndex;
|
||||
use crate::report::{RfcRef, Warning};
|
||||
use crate::storage::{
|
||||
PackFile, PackTime, RawByHashEntry, RocksStore, ValidatedCaInstanceResult, VcirArtifactKind,
|
||||
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirChildEntry,
|
||||
VcirInstanceGate, VcirLocalOutput, VcirOutputType, VcirRelatedArtifact, VcirSummary,
|
||||
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection,
|
||||
VcirChildEntry, VcirInstanceGate, VcirLocalOutput, VcirOutputType, VcirRelatedArtifact,
|
||||
VcirSummary,
|
||||
};
|
||||
use crate::sync::repo::{
|
||||
sync_publication_point, sync_publication_point_replay, sync_publication_point_replay_delta,
|
||||
@ -49,6 +51,7 @@ use crate::validation::publication_point::PublicationPointSnapshot;
|
||||
use crate::validation::tree::{
|
||||
CaInstanceHandle, DiscoveredChildCaInstance, PublicationPointRunResult, PublicationPointRunner,
|
||||
};
|
||||
use sha2::Digest;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
@ -134,9 +137,47 @@ pub struct Rpkiv1PublicationPointRunner<'a> {
|
||||
pub repo_sync_runtime: Option<Arc<dyn RepoSyncRuntime>>,
|
||||
pub parallel_phase2_config: Option<ParallelPhase2Config>,
|
||||
pub parallel_roa_worker_pool: Option<ParallelRoaWorkerPool>,
|
||||
pub ccr_accumulator: Option<Mutex<CcrAccumulator>>,
|
||||
}
|
||||
|
||||
impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
pub(crate) fn ccr_accumulator_snapshot(&self) -> Option<CcrAccumulator> {
|
||||
self.ccr_accumulator
|
||||
.as_ref()
|
||||
.and_then(|accumulator| accumulator.lock().ok().map(|guard| guard.clone()))
|
||||
}
|
||||
|
||||
pub(crate) fn append_ccr_manifest_projection(
|
||||
&self,
|
||||
projection: &VcirCcrManifestProjection,
|
||||
) -> Result<(), String> {
|
||||
if let Some(accumulator) = self.ccr_accumulator.as_ref() {
|
||||
accumulator
|
||||
.lock()
|
||||
.map_err(|_| "lock CCR accumulator failed".to_string())?
|
||||
.append_manifest_projection(projection)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn append_ccr_manifest_projection_from_reuse(
|
||||
&self,
|
||||
projection: &VcirReuseProjection,
|
||||
) -> Result<(), String> {
|
||||
match projection.source {
|
||||
PublicationPointSource::Fresh => Err(
|
||||
"invalid reuse projection source: fresh does not belong to failed-fetch reuse"
|
||||
.to_string(),
|
||||
),
|
||||
PublicationPointSource::VcirCurrentInstance => self.append_ccr_manifest_projection(
|
||||
projection.ccr_manifest_projection.as_ref().ok_or_else(|| {
|
||||
"vcir current-instance reuse is missing CCR manifest projection".to_string()
|
||||
})?,
|
||||
),
|
||||
PublicationPointSource::FailedFetchNoCache => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn stage_fresh_publication_point_after_repo_ready(
|
||||
&self,
|
||||
ca: &CaInstanceHandle,
|
||||
@ -237,6 +278,14 @@ impl<'a> Rpkiv1PublicationPointRunner<'a> {
|
||||
.map_err(|e| format!("persist VCIR failed: {e}"))?;
|
||||
let persist_vcir_ms = persist_vcir_started.elapsed().as_millis() as u64;
|
||||
|
||||
if self.ccr_accumulator.is_some() {
|
||||
let child_entries =
|
||||
build_vcir_child_entries(&discovered_children, self.validation_time)?;
|
||||
let ccr_manifest_projection =
|
||||
build_vcir_ccr_manifest_projection_from_fresh(ca, &pack, &child_entries)?;
|
||||
self.append_ccr_manifest_projection(&ccr_manifest_projection)?;
|
||||
}
|
||||
|
||||
let audit_build_started = std::time::Instant::now();
|
||||
let audit = build_publication_point_audit_from_snapshot(
|
||||
ca,
|
||||
@ -745,6 +794,7 @@ impl<'a> PublicationPointRunner for Rpkiv1PublicationPointRunner<'a> {
|
||||
self.validation_time,
|
||||
)
|
||||
.map_err(|e| format!("failed fetch VCIR projection failed: {e}"))?;
|
||||
self.append_ccr_manifest_projection_from_reuse(&projection)?;
|
||||
let projection_ms = projection_started.elapsed().as_millis() as u64;
|
||||
warnings.extend(projection.warnings.clone());
|
||||
let audit_build_started = std::time::Instant::now();
|
||||
@ -888,6 +938,7 @@ enum CachedIssuerCrl {
|
||||
struct VcirReuseProjection {
|
||||
source: PublicationPointSource,
|
||||
vcir: Option<ValidatedCaInstanceResult>,
|
||||
ccr_manifest_projection: Option<VcirCcrManifestProjection>,
|
||||
snapshot: Option<PublicationPointSnapshot>,
|
||||
objects: crate::validation::objects::ObjectsOutput,
|
||||
child_audits: Vec<ObjectAuditEntry>,
|
||||
@ -1872,6 +1923,19 @@ fn empty_objects_output() -> crate::validation::objects::ObjectsOutput {
|
||||
}
|
||||
}
|
||||
|
||||
fn reuse_ccr_manifest_projection_from_vcir(
|
||||
ca: &CaInstanceHandle,
|
||||
vcir: &ValidatedCaInstanceResult,
|
||||
) -> Result<VcirCcrManifestProjection, String> {
|
||||
if vcir.ccr_manifest_projection.manifest_rsync_uri != ca.manifest_rsync_uri {
|
||||
return Err(format!(
|
||||
"vcir CCR manifest projection URI mismatch: expected {}, got {}",
|
||||
ca.manifest_rsync_uri, vcir.ccr_manifest_projection.manifest_rsync_uri
|
||||
));
|
||||
}
|
||||
Ok(vcir.ccr_manifest_projection.clone())
|
||||
}
|
||||
|
||||
fn project_current_instance_vcir_on_failed_fetch(
|
||||
store: &RocksStore,
|
||||
ca: &CaInstanceHandle,
|
||||
@ -1896,6 +1960,7 @@ fn project_current_instance_vcir_on_failed_fetch(
|
||||
return Ok(VcirReuseProjection {
|
||||
source: PublicationPointSource::FailedFetchNoCache,
|
||||
vcir: None,
|
||||
ccr_manifest_projection: None,
|
||||
snapshot: None,
|
||||
objects: empty_objects_output(),
|
||||
child_audits: Vec::new(),
|
||||
@ -1915,6 +1980,7 @@ fn project_current_instance_vcir_on_failed_fetch(
|
||||
return Ok(VcirReuseProjection {
|
||||
source: PublicationPointSource::FailedFetchNoCache,
|
||||
vcir: Some(vcir),
|
||||
ccr_manifest_projection: None,
|
||||
snapshot: None,
|
||||
objects: empty_objects_output(),
|
||||
child_audits: Vec::new(),
|
||||
@ -1936,6 +2002,7 @@ fn project_current_instance_vcir_on_failed_fetch(
|
||||
return Ok(VcirReuseProjection {
|
||||
source: PublicationPointSource::FailedFetchNoCache,
|
||||
vcir: Some(vcir),
|
||||
ccr_manifest_projection: None,
|
||||
snapshot: None,
|
||||
objects: empty_objects_output(),
|
||||
child_audits: Vec::new(),
|
||||
@ -1950,6 +2017,7 @@ fn project_current_instance_vcir_on_failed_fetch(
|
||||
.with_context(&ca.manifest_rsync_uri),
|
||||
);
|
||||
|
||||
let ccr_manifest_projection = reuse_ccr_manifest_projection_from_vcir(ca, &vcir)?;
|
||||
let snapshot = reconstruct_snapshot_from_vcir(store, ca, &vcir, &mut warnings);
|
||||
let objects = build_objects_output_from_vcir(&vcir, validation_time, &mut warnings);
|
||||
let (discovered_children, child_audits) =
|
||||
@ -1958,6 +2026,7 @@ fn project_current_instance_vcir_on_failed_fetch(
|
||||
Ok(VcirReuseProjection {
|
||||
source: PublicationPointSource::VcirCurrentInstance,
|
||||
vcir: Some(vcir),
|
||||
ccr_manifest_projection: Some(ccr_manifest_projection),
|
||||
snapshot,
|
||||
objects,
|
||||
child_audits,
|
||||
@ -2510,6 +2579,8 @@ fn build_vcir_from_fresh_result_with_timing(
|
||||
child_audits,
|
||||
);
|
||||
timing.related_artifacts_ms = related_artifacts_started.elapsed().as_millis() as u64;
|
||||
let ccr_manifest_projection =
|
||||
build_vcir_ccr_manifest_projection_from_fresh(ca, pack, &child_entries)?;
|
||||
let accepted_object_count = related_artifacts
|
||||
.iter()
|
||||
.filter(|artifact| artifact.validation_status == VcirArtifactValidationStatus::Accepted)
|
||||
@ -2552,6 +2623,7 @@ fn build_vcir_from_fresh_result_with_timing(
|
||||
validated_manifest_this_update: pack.this_update.clone(),
|
||||
validated_manifest_next_update: pack.next_update.clone(),
|
||||
},
|
||||
ccr_manifest_projection,
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: pack.next_update.clone(),
|
||||
current_crl_next_update: PackTime::from_utc_offset_datetime(
|
||||
@ -2598,6 +2670,138 @@ fn build_vcir_from_fresh_result_with_timing(
|
||||
Ok((vcir, timing))
|
||||
}
|
||||
|
||||
fn build_vcir_ccr_manifest_projection_from_fresh(
|
||||
ca: &CaInstanceHandle,
|
||||
pack: &PublicationPointSnapshot,
|
||||
child_entries: &[VcirChildEntry],
|
||||
) -> Result<VcirCcrManifestProjection, String> {
|
||||
let manifest = ManifestObject::decode_der(&pack.manifest_bytes)
|
||||
.map_err(|e| format!("decode manifest for VCIR CCR projection failed: {e}"))?;
|
||||
let ee = &manifest.signed_object.signed_data.certificates[0].resource_cert;
|
||||
let manifest_ee_aki = ee
|
||||
.tbs
|
||||
.extensions
|
||||
.authority_key_identifier
|
||||
.clone()
|
||||
.ok_or_else(|| "manifest EE certificate missing AuthorityKeyIdentifier".to_string())?;
|
||||
let manifest_sia_locations_der = match ee
|
||||
.tbs
|
||||
.extensions
|
||||
.subject_info_access
|
||||
.as_ref()
|
||||
.ok_or_else(|| "manifest EE certificate missing Subject Information Access".to_string())?
|
||||
{
|
||||
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
||||
.access_descriptions
|
||||
.iter()
|
||||
.map(encode_access_description_der_for_vcir_ccr_projection)
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
SubjectInfoAccess::Ca(_) => {
|
||||
return Err(
|
||||
"manifest EE certificate Subject Information Access has CA variant".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let mut subordinate_skis = child_entries
|
||||
.iter()
|
||||
.map(|child| {
|
||||
hex::decode(&child.child_ski)
|
||||
.map_err(|e| format!("decode child_ski for VCIR CCR projection failed: {e}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
subordinate_skis.sort();
|
||||
subordinate_skis.dedup();
|
||||
|
||||
Ok(VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: ca.manifest_rsync_uri.clone(),
|
||||
manifest_sha256: sha2::Sha256::digest(&pack.manifest_bytes).to_vec(),
|
||||
manifest_size: pack.manifest_bytes.len() as u64,
|
||||
manifest_ee_aki,
|
||||
manifest_number_be: pack.manifest_number_be.clone(),
|
||||
manifest_this_update: pack.this_update.clone(),
|
||||
manifest_sia_locations_der,
|
||||
subordinate_skis,
|
||||
})
|
||||
}
|
||||
|
||||
fn encode_access_description_der_for_vcir_ccr_projection(
|
||||
access_description: &AccessDescription,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
let oid = encode_oid_der_for_vcir_ccr_projection(&access_description.access_method_oid)?;
|
||||
let uri = encode_tlv_for_vcir_ccr_projection(
|
||||
0x86,
|
||||
access_description.access_location.as_bytes().to_vec(),
|
||||
);
|
||||
Ok(encode_sequence_for_vcir_ccr_projection(&[oid, uri]))
|
||||
}
|
||||
|
||||
fn encode_oid_der_for_vcir_ccr_projection(oid: &str) -> Result<Vec<u8>, String> {
|
||||
let arcs = oid
|
||||
.split('.')
|
||||
.map(|part| {
|
||||
part.parse::<u64>()
|
||||
.map_err(|_| format!("unsupported accessMethod OID: {oid}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
if arcs.len() < 2 {
|
||||
return Err(format!("unsupported accessMethod OID: {oid}"));
|
||||
}
|
||||
if arcs[0] > 2 || (arcs[0] < 2 && arcs[1] >= 40) {
|
||||
return Err(format!("unsupported accessMethod OID: {oid}"));
|
||||
}
|
||||
let mut body = Vec::new();
|
||||
body.push((arcs[0] * 40 + arcs[1]) as u8);
|
||||
for arc in &arcs[2..] {
|
||||
encode_base128_for_vcir_ccr_projection(*arc, &mut body);
|
||||
}
|
||||
Ok(encode_tlv_for_vcir_ccr_projection(0x06, body))
|
||||
}
|
||||
|
||||
fn encode_base128_for_vcir_ccr_projection(mut value: u64, out: &mut Vec<u8>) {
|
||||
let mut tmp = vec![(value & 0x7F) as u8];
|
||||
value >>= 7;
|
||||
while value > 0 {
|
||||
tmp.push(((value & 0x7F) as u8) | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
tmp.reverse();
|
||||
out.extend_from_slice(&tmp);
|
||||
}
|
||||
|
||||
fn encode_sequence_for_vcir_ccr_projection(elements: &[Vec<u8>]) -> Vec<u8> {
|
||||
let total_len: usize = elements.iter().map(Vec::len).sum();
|
||||
let mut buf = Vec::with_capacity(total_len);
|
||||
for element in elements {
|
||||
buf.extend_from_slice(element);
|
||||
}
|
||||
encode_tlv_for_vcir_ccr_projection(0x30, buf)
|
||||
}
|
||||
|
||||
fn encode_tlv_for_vcir_ccr_projection(tag: u8, value: Vec<u8>) -> Vec<u8> {
|
||||
let mut out = Vec::with_capacity(1 + 9 + value.len());
|
||||
out.push(tag);
|
||||
encode_length_for_vcir_ccr_projection(value.len(), &mut out);
|
||||
out.extend_from_slice(&value);
|
||||
out
|
||||
}
|
||||
|
||||
fn encode_length_for_vcir_ccr_projection(len: usize, out: &mut Vec<u8>) {
|
||||
if len < 0x80 {
|
||||
out.push(len as u8);
|
||||
return;
|
||||
}
|
||||
let mut bytes = Vec::new();
|
||||
let mut value = len;
|
||||
while value > 0 {
|
||||
bytes.push((value & 0xFF) as u8);
|
||||
value >>= 8;
|
||||
}
|
||||
bytes.reverse();
|
||||
out.push(0x80 | (bytes.len() as u8));
|
||||
out.extend_from_slice(&bytes);
|
||||
}
|
||||
|
||||
struct CurrentCrlRef<'a> {
|
||||
file: &'a PackFile,
|
||||
crl: RpkixCrl,
|
||||
@ -3048,6 +3252,32 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn sample_runner_with_ccr_accumulator<'a>(
|
||||
store: &'a RocksStore,
|
||||
policy: &'a Policy,
|
||||
) -> Rpkiv1PublicationPointRunner<'a> {
|
||||
Rpkiv1PublicationPointRunner {
|
||||
store,
|
||||
policy,
|
||||
http_fetcher: &NeverHttpFetcher,
|
||||
rsync_fetcher: &FailingRsyncFetcher,
|
||||
validation_time: time::OffsetDateTime::now_utc(),
|
||||
timing: None,
|
||||
download_log: None,
|
||||
replay_archive_index: None,
|
||||
replay_delta_index: None,
|
||||
rrdp_dedup: false,
|
||||
rrdp_repo_cache: Mutex::new(HashMap::new()),
|
||||
rsync_dedup: false,
|
||||
rsync_repo_cache: Mutex::new(HashMap::new()),
|
||||
current_repo_index: None,
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: Some(Mutex::new(CcrAccumulator::new(Vec::new()))),
|
||||
}
|
||||
}
|
||||
|
||||
fn openssl_available() -> bool {
|
||||
Command::new("openssl")
|
||||
.arg("version")
|
||||
@ -3509,6 +3739,19 @@ authorityKeyIdentifier = keyid:always
|
||||
let router_hash = sha256_hex(b"router-bytes");
|
||||
let ee_hash = sha256_hex(b"ee-cert-bytes");
|
||||
let gate_until = PackTime::from_utc_offset_datetime(now + time::Duration::hours(1));
|
||||
let ccr_manifest_projection = VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: manifest_uri.clone(),
|
||||
manifest_sha256: hex::decode(&manifest_hash).expect("decode manifest hash"),
|
||||
manifest_size: 2048,
|
||||
manifest_ee_aki: vec![0x11; 20],
|
||||
manifest_number_be: vec![1],
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis: vec![vec![0x33; 20]],
|
||||
};
|
||||
ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: manifest_uri.clone(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
@ -3524,6 +3767,7 @@ authorityKeyIdentifier = keyid:always
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
||||
validated_manifest_next_update: gate_until.clone(),
|
||||
},
|
||||
ccr_manifest_projection,
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: gate_until.clone(),
|
||||
current_crl_next_update: gate_until.clone(),
|
||||
@ -3901,6 +4145,22 @@ authorityKeyIdentifier = keyid:always
|
||||
.expect("vcir exists");
|
||||
assert_eq!(vcir.manifest_rsync_uri, pack.manifest_rsync_uri);
|
||||
assert_eq!(vcir.summary.local_vrp_count as usize, objects.vrps.len());
|
||||
assert_eq!(
|
||||
vcir.ccr_manifest_projection.manifest_rsync_uri,
|
||||
pack.manifest_rsync_uri
|
||||
);
|
||||
assert_eq!(
|
||||
vcir.ccr_manifest_projection.manifest_number_be,
|
||||
pack.manifest_number_be
|
||||
);
|
||||
assert_eq!(
|
||||
vcir.ccr_manifest_projection.manifest_this_update,
|
||||
pack.this_update
|
||||
);
|
||||
assert_eq!(
|
||||
vcir.ccr_manifest_projection.manifest_size,
|
||||
pack.manifest_bytes.len() as u64
|
||||
);
|
||||
let first_output = vcir.local_outputs.first().expect("local outputs stored");
|
||||
assert!(store
|
||||
.get_audit_rule_index_entry(crate::storage::AuditRuleKind::Roa, &first_output.rule_hash)
|
||||
@ -3908,6 +4168,81 @@ authorityKeyIdentifier = keyid:always
|
||||
.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_vcir_ccr_manifest_projection_from_fresh_real_snapshot_matches_manifest_contents() {
|
||||
let (pack, issuer_ca_der, validation_time) =
|
||||
cernet_publication_point_snapshot_for_vcir_tests();
|
||||
let issuer_ca = ResourceCertificate::decode_der(&issuer_ca_der).expect("decode issuer ca");
|
||||
let ca = CaInstanceHandle {
|
||||
depth: 0,
|
||||
tal_id: "test-tal".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
ca_certificate_der: issuer_ca_der,
|
||||
ca_certificate_rsync_uri: Some(
|
||||
"rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string(),
|
||||
),
|
||||
effective_ip_resources: issuer_ca.tbs.extensions.ip_resources.clone(),
|
||||
effective_as_resources: issuer_ca.tbs.extensions.as_resources.clone(),
|
||||
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
|
||||
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
let child_discovery =
|
||||
discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None)
|
||||
.expect("discover children");
|
||||
let child_entries = build_vcir_child_entries(&child_discovery.children, validation_time)
|
||||
.expect("build child entries");
|
||||
|
||||
let projection = build_vcir_ccr_manifest_projection_from_fresh(&ca, &pack, &child_entries)
|
||||
.expect("build ccr manifest projection");
|
||||
let manifest = ManifestObject::decode_der(&pack.manifest_bytes).expect("decode manifest");
|
||||
let expected_locations = match manifest.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.subject_info_access
|
||||
.as_ref()
|
||||
.expect("manifest sia")
|
||||
{
|
||||
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
||||
.access_descriptions
|
||||
.iter()
|
||||
.map(encode_access_description_der_for_vcir_ccr_projection)
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.expect("encode locations"),
|
||||
SubjectInfoAccess::Ca(_) => panic!("manifest ee SIA should not be CA variant"),
|
||||
};
|
||||
|
||||
assert_eq!(projection.manifest_rsync_uri, pack.manifest_rsync_uri);
|
||||
assert_eq!(
|
||||
projection.manifest_sha256,
|
||||
sha2::Sha256::digest(&pack.manifest_bytes).to_vec()
|
||||
);
|
||||
assert_eq!(projection.manifest_size, pack.manifest_bytes.len() as u64);
|
||||
assert_eq!(
|
||||
projection.manifest_ee_aki,
|
||||
manifest.signed_object.signed_data.certificates[0]
|
||||
.resource_cert
|
||||
.tbs
|
||||
.extensions
|
||||
.authority_key_identifier
|
||||
.clone()
|
||||
.expect("manifest aki")
|
||||
);
|
||||
assert_eq!(
|
||||
projection.manifest_number_be,
|
||||
manifest.manifest.manifest_number.bytes_be
|
||||
);
|
||||
assert_eq!(projection.manifest_this_update, pack.this_update);
|
||||
assert_eq!(projection.manifest_sia_locations_der, expected_locations);
|
||||
let expected_subordinate_skis = child_entries
|
||||
.iter()
|
||||
.map(|child| hex::decode(&child.child_ski).expect("decode child ski"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(projection.subordinate_skis, expected_subordinate_skis);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_vcir_related_artifacts_classifies_snapshot_files_and_audit_statuses() {
|
||||
let manifest_bytes = std::fs::read(
|
||||
@ -4243,6 +4578,7 @@ authorityKeyIdentifier = keyid:always
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
// For this fixture-driven smoke, we provide the correct issuer CA certificate (the CA for
|
||||
@ -4404,6 +4740,7 @@ authorityKeyIdentifier = keyid:always
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let first = runner.run_publication_point(&handle).expect("first run ok");
|
||||
@ -4515,6 +4852,7 @@ authorityKeyIdentifier = keyid:always
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let first = runner.run_publication_point(&handle).expect("first run ok");
|
||||
@ -4629,6 +4967,7 @@ authorityKeyIdentifier = keyid:always
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let first = runner.run_publication_point(&handle).expect("first run ok");
|
||||
@ -4715,6 +5054,7 @@ authorityKeyIdentifier = keyid:always
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
let first = ok_runner
|
||||
.run_publication_point(&handle)
|
||||
@ -4744,6 +5084,7 @@ authorityKeyIdentifier = keyid:always
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
let second = bad_runner
|
||||
.run_publication_point(&handle)
|
||||
@ -5274,6 +5615,10 @@ authorityKeyIdentifier = keyid:always
|
||||
projection.discovered_children[0].handle.manifest_rsync_uri,
|
||||
"rsync://example.test/repo/child/child.mft"
|
||||
);
|
||||
assert_eq!(
|
||||
projection.ccr_manifest_projection.as_ref(),
|
||||
Some(&vcir.ccr_manifest_projection)
|
||||
);
|
||||
assert!(
|
||||
projection.snapshot.is_some(),
|
||||
"expected reconstructed snapshot"
|
||||
@ -5325,6 +5670,7 @@ authorityKeyIdentifier = keyid:always
|
||||
projection.source,
|
||||
PublicationPointSource::FailedFetchNoCache
|
||||
);
|
||||
assert!(projection.ccr_manifest_projection.is_none());
|
||||
assert!(projection.objects.vrps.is_empty());
|
||||
assert!(projection.objects.aspas.is_empty());
|
||||
assert!(projection.discovered_children.is_empty());
|
||||
@ -5364,6 +5710,7 @@ authorityKeyIdentifier = keyid:always
|
||||
PublicationPointSource::FailedFetchNoCache
|
||||
);
|
||||
assert!(projection.vcir.is_none());
|
||||
assert!(projection.ccr_manifest_projection.is_none());
|
||||
assert!(projection.snapshot.is_none());
|
||||
assert!(projection.objects.audit.is_empty());
|
||||
assert!(projection.discovered_children.is_empty());
|
||||
@ -5414,6 +5761,7 @@ authorityKeyIdentifier = keyid:always
|
||||
PublicationPointSource::FailedFetchNoCache
|
||||
);
|
||||
assert!(projection.vcir.is_some());
|
||||
assert!(projection.ccr_manifest_projection.is_none());
|
||||
assert!(projection.snapshot.is_none());
|
||||
assert!(projection.discovered_children.is_empty());
|
||||
assert!(projection.warnings.iter().any(|warning| {
|
||||
@ -5423,6 +5771,144 @@ authorityKeyIdentifier = keyid:always
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn project_current_instance_vcir_rejects_mismatched_ccr_projection_uri() {
|
||||
let now = time::OffsetDateTime::now_utc();
|
||||
let child_cert_hash = sha256_hex(b"child-cert");
|
||||
let mut vcir = sample_vcir_for_projection(now, &child_cert_hash);
|
||||
vcir.ccr_manifest_projection.manifest_rsync_uri =
|
||||
"rsync://example.test/repo/issuer/other.mft".to_string();
|
||||
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
store.put_vcir(&vcir).expect("put vcir");
|
||||
|
||||
let ca = CaInstanceHandle {
|
||||
depth: 0,
|
||||
tal_id: "test-tal".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
ca_certificate_der: Vec::new(),
|
||||
ca_certificate_rsync_uri: None,
|
||||
effective_ip_resources: None,
|
||||
effective_as_resources: None,
|
||||
rsync_base_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
manifest_rsync_uri: vcir.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: "rsync://example.test/repo/issuer/".to_string(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
|
||||
let err = project_current_instance_vcir_on_failed_fetch(
|
||||
&store,
|
||||
&ca,
|
||||
&ManifestFreshError::RepoSyncFailed {
|
||||
detail: "synthetic".to_string(),
|
||||
},
|
||||
now,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(
|
||||
err.contains("vcir CCR manifest projection URI mismatch"),
|
||||
"{err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fresh_and_reuse_paths_produce_equivalent_ccr_manifest_projection() {
|
||||
let (pack, issuer_ca_der, validation_time) =
|
||||
cernet_publication_point_snapshot_for_vcir_tests();
|
||||
let ca = CaInstanceHandle {
|
||||
depth: 0,
|
||||
tal_id: "test-tal".to_string(),
|
||||
parent_manifest_rsync_uri: None,
|
||||
ca_certificate_der: issuer_ca_der,
|
||||
ca_certificate_rsync_uri: Some("rsync://rpki.apnic.net/repository/B527EF581D6611E2BB468F7C72FD1FF2/BfycW4hQb3wNP4YsiJW-1n6fjro.cer".to_string()),
|
||||
effective_ip_resources: None,
|
||||
effective_as_resources: None,
|
||||
rsync_base_uri: pack.publication_point_rsync_uri.clone(),
|
||||
manifest_rsync_uri: pack.manifest_rsync_uri.clone(),
|
||||
publication_point_rsync_uri: pack.publication_point_rsync_uri.clone(),
|
||||
rrdp_notification_uri: None,
|
||||
};
|
||||
let child_discovery =
|
||||
discover_children_from_fresh_snapshot_with_audit(&ca, &pack, validation_time, None)
|
||||
.expect("discover children");
|
||||
let fresh_vcir = build_vcir_from_fresh_result(
|
||||
&ca,
|
||||
&pack,
|
||||
&empty_objects_output(),
|
||||
&[],
|
||||
&child_discovery.audits,
|
||||
&child_discovery.children,
|
||||
validation_time,
|
||||
)
|
||||
.expect("build fresh vcir");
|
||||
|
||||
let reuse_projection = reuse_ccr_manifest_projection_from_vcir(&ca, &fresh_vcir)
|
||||
.expect("reuse projection from vcir");
|
||||
|
||||
assert_eq!(fresh_vcir.ccr_manifest_projection, reuse_projection);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_ccr_manifest_projection_from_reuse_requires_projection_for_current_instance() {
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
let policy = Policy::default();
|
||||
let runner = sample_runner_with_ccr_accumulator(&store, &policy);
|
||||
|
||||
let err = runner
|
||||
.append_ccr_manifest_projection_from_reuse(&VcirReuseProjection {
|
||||
source: PublicationPointSource::VcirCurrentInstance,
|
||||
vcir: None,
|
||||
ccr_manifest_projection: None,
|
||||
snapshot: None,
|
||||
objects: empty_objects_output(),
|
||||
child_audits: Vec::new(),
|
||||
discovered_children: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
})
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err.contains("missing CCR manifest projection"), "{err}");
|
||||
assert_eq!(
|
||||
runner
|
||||
.ccr_accumulator_snapshot()
|
||||
.expect("ccr accumulator snapshot")
|
||||
.manifest_count(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_ccr_manifest_projection_from_reuse_skips_failed_fetch_no_cache() {
|
||||
let store_dir = tempfile::tempdir().expect("store dir");
|
||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||
let policy = Policy::default();
|
||||
let runner = sample_runner_with_ccr_accumulator(&store, &policy);
|
||||
|
||||
runner
|
||||
.append_ccr_manifest_projection_from_reuse(&VcirReuseProjection {
|
||||
source: PublicationPointSource::FailedFetchNoCache,
|
||||
vcir: None,
|
||||
ccr_manifest_projection: None,
|
||||
snapshot: None,
|
||||
objects: empty_objects_output(),
|
||||
child_audits: Vec::new(),
|
||||
discovered_children: Vec::new(),
|
||||
warnings: Vec::new(),
|
||||
})
|
||||
.expect("failed-fetch no-cache should not append");
|
||||
|
||||
assert_eq!(
|
||||
runner
|
||||
.ccr_accumulator_snapshot()
|
||||
.expect("ccr accumulator snapshot")
|
||||
.manifest_count(),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_snapshot_time_value_reports_invalid_timestamp() {
|
||||
let err = parse_snapshot_time_value(&PackTime {
|
||||
@ -5919,6 +6405,7 @@ authorityKeyIdentifier = keyid:always
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
let first = runner_rrdp
|
||||
.run_publication_point(&handle)
|
||||
@ -5951,6 +6438,7 @@ authorityKeyIdentifier = keyid:always
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
let third = runner_rsync
|
||||
.run_publication_point(&handle)
|
||||
|
||||
@ -184,6 +184,7 @@ fn apnic_tree_full_stats_serial() {
|
||||
repo_sync_runtime: None,
|
||||
parallel_phase2_config: None,
|
||||
parallel_roa_worker_pool: None,
|
||||
ccr_accumulator: None,
|
||||
};
|
||||
|
||||
let stats = RefCell::new(LiveStats::default());
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
use rpki::ccr::{
|
||||
CcrContentInfo, CcrDigestAlgorithm, ManifestInstance, ManifestState, RoaPayloadSet,
|
||||
RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState, TrustAnchorState, compute_state_hash,
|
||||
decode_content_info, dump_content_info_json_value,
|
||||
compute_state_hash, decode_content_info, dump_content_info_json_value,
|
||||
encode::{
|
||||
encode_aspa_payload_state_payload_der, encode_content_info,
|
||||
encode_manifest_state_payload_der, encode_roa_payload_state_payload_der,
|
||||
@ -10,12 +8,14 @@ use rpki::ccr::{
|
||||
verify::{
|
||||
verify_against_report_json_path, verify_against_vcir_store, verify_content_info_bytes,
|
||||
},
|
||||
CcrContentInfo, CcrDigestAlgorithm, ManifestInstance, ManifestState, RoaPayloadSet,
|
||||
RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState, TrustAnchorState,
|
||||
};
|
||||
use rpki::data_model::common::BigUnsigned;
|
||||
use rpki::storage::{
|
||||
PackTime, RocksStore, ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind,
|
||||
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirChildEntry,
|
||||
VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||
VcirArtifactRole, VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection,
|
||||
VcirChildEntry, VcirInstanceGate, VcirRelatedArtifact, VcirSummary,
|
||||
};
|
||||
|
||||
fn sample_time() -> time::OffsetDateTime {
|
||||
@ -108,6 +108,25 @@ fn sample_ccr() -> Vec<u8> {
|
||||
encode_content_info(&CcrContentInfo::new(ccr)).expect("encode ccr")
|
||||
}
|
||||
|
||||
fn sample_ccr_manifest_projection(
|
||||
manifest_rsync_uri: &str,
|
||||
manifest_sha256: Vec<u8>,
|
||||
) -> VcirCcrManifestProjection {
|
||||
VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
manifest_sha256,
|
||||
manifest_size: 2048,
|
||||
manifest_ee_aki: vec![0x55; 20],
|
||||
manifest_number_be: vec![1],
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis: vec![vec![0x33; 20]],
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_content_info_bytes_succeeds_for_valid_ccr() {
|
||||
let der = sample_ccr();
|
||||
@ -195,6 +214,10 @@ fn verify_against_vcir_store_matches_manifest_hashes() {
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
validated_manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
},
|
||||
ccr_manifest_projection: sample_ccr_manifest_projection(
|
||||
"rsync://example.test/current.mft",
|
||||
vec![0x10; 32],
|
||||
),
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
current_crl_next_update: PackTime::from_utc_offset_datetime(sample_time()),
|
||||
|
||||
@ -7,10 +7,10 @@ use rpki::policy::{CaFailedFetchPolicy, Policy};
|
||||
use rpki::storage::{
|
||||
PackTime, RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore,
|
||||
ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole,
|
||||
VcirArtifactValidationStatus, VcirAuditSummary, VcirInstanceGate, VcirRelatedArtifact,
|
||||
VcirSummary,
|
||||
VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirInstanceGate,
|
||||
VcirRelatedArtifact, VcirSummary,
|
||||
};
|
||||
use rpki::validation::manifest::{PublicationPointSource, process_manifest_publication_point};
|
||||
use rpki::validation::manifest::{process_manifest_publication_point, PublicationPointSource};
|
||||
|
||||
fn issuer_ca_fixture() -> Vec<u8> {
|
||||
std::fs::read(
|
||||
@ -64,6 +64,19 @@ fn store_validated_manifest_baseline(
|
||||
store
|
||||
.put_raw_by_hash_entry(&manifest_raw)
|
||||
.expect("store VCIR manifest raw_by_hash");
|
||||
let ccr_manifest_projection = VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
manifest_sha256: hex::decode(&manifest_sha256).expect("decode manifest hash"),
|
||||
manifest_size: manifest_bytes.len() as u64,
|
||||
manifest_ee_aki: vec![0x11; 20],
|
||||
manifest_number_be: manifest_number_be.clone(),
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(this_update),
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis: Vec::new(),
|
||||
};
|
||||
|
||||
let vcir = ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
@ -80,6 +93,7 @@ fn store_validated_manifest_baseline(
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(this_update),
|
||||
validated_manifest_next_update: PackTime::from_utc_offset_datetime(next_update),
|
||||
},
|
||||
ccr_manifest_projection,
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: PackTime::from_utc_offset_datetime(next_update),
|
||||
current_crl_next_update: PackTime::from_utc_offset_datetime(next_update),
|
||||
|
||||
@ -7,12 +7,12 @@ use rpki::policy::{CaFailedFetchPolicy, Policy};
|
||||
use rpki::storage::{
|
||||
PackTime, RawByHashEntry, RepositoryViewEntry, RepositoryViewState, RocksStore,
|
||||
ValidatedCaInstanceResult, ValidatedManifestMeta, VcirArtifactKind, VcirArtifactRole,
|
||||
VcirArtifactValidationStatus, VcirAuditSummary, VcirInstanceGate, VcirRelatedArtifact,
|
||||
VcirSummary,
|
||||
VcirArtifactValidationStatus, VcirAuditSummary, VcirCcrManifestProjection, VcirInstanceGate,
|
||||
VcirRelatedArtifact, VcirSummary,
|
||||
};
|
||||
use rpki::validation::manifest::{
|
||||
ManifestProcessError, PublicationPointSource, process_manifest_publication_point,
|
||||
process_manifest_publication_point_after_repo_sync,
|
||||
process_manifest_publication_point, process_manifest_publication_point_after_repo_sync,
|
||||
ManifestProcessError, PublicationPointSource,
|
||||
};
|
||||
|
||||
fn issuer_ca_fixture_der() -> Vec<u8> {
|
||||
@ -67,6 +67,19 @@ fn store_validated_manifest_baseline(
|
||||
store
|
||||
.put_raw_by_hash_entry(&manifest_raw)
|
||||
.expect("store VCIR manifest raw_by_hash");
|
||||
let ccr_manifest_projection = VcirCcrManifestProjection {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
manifest_sha256: hex::decode(&manifest_sha256).expect("decode manifest hash"),
|
||||
manifest_size: manifest_bytes.len() as u64,
|
||||
manifest_ee_aki: vec![0x11; 20],
|
||||
manifest_number_be: manifest_number_be.clone(),
|
||||
manifest_this_update: PackTime::from_utc_offset_datetime(this_update),
|
||||
manifest_sia_locations_der: vec![vec![
|
||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
||||
b'r', b's', b'y', b'n', b'c',
|
||||
]],
|
||||
subordinate_skis: Vec::new(),
|
||||
};
|
||||
|
||||
let vcir = ValidatedCaInstanceResult {
|
||||
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
||||
@ -83,6 +96,7 @@ fn store_validated_manifest_baseline(
|
||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(this_update),
|
||||
validated_manifest_next_update: PackTime::from_utc_offset_datetime(next_update),
|
||||
},
|
||||
ccr_manifest_projection,
|
||||
instance_gate: VcirInstanceGate {
|
||||
manifest_next_update: PackTime::from_utc_offset_datetime(next_update),
|
||||
current_crl_next_update: PackTime::from_utc_offset_datetime(next_update),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user