20260720 规范化 CCR Manifest location 导出

This commit is contained in:
yuyr 2026-07-20 16:24:47 +08:00
parent 06f4b086b4
commit daf0764cd0
12 changed files with 940 additions and 247 deletions

View File

@ -0,0 +1,107 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
check_rpki_client_ccr.sh \
--rpki-client <path> \
--ccr-root <directory-or-ccr-file> \
--out <result.jsonl> \
[--limit <count>]
Runs the supplied rpki-client importer against CCR files. A non-zero result
means that rpki-client reported a CCR-file parsing error or could not start.
Per-file command output is retained next to the JSONL result file.
EOF
}
RPKI_CLIENT=""
CCR_ROOT=""
OUT=""
LIMIT=0
while [[ $# -gt 0 ]]; do
case "$1" in
--rpki-client) RPKI_CLIENT=${2:?missing value for --rpki-client}; shift 2 ;;
--ccr-root) CCR_ROOT=${2:?missing value for --ccr-root}; shift 2 ;;
--out) OUT=${2:?missing value for --out}; shift 2 ;;
--limit) LIMIT=${2:?missing value for --limit}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown argument: $1" >&2; usage >&2; exit 2 ;;
esac
done
[[ -n "$RPKI_CLIENT" && -n "$CCR_ROOT" && -n "$OUT" ]] || {
echo "error: --rpki-client, --ccr-root, and --out are required" >&2
usage >&2
exit 2
}
[[ -x "$RPKI_CLIENT" ]] || { echo "error: rpki-client is not executable: $RPKI_CLIENT" >&2; exit 2; }
[[ "$LIMIT" =~ ^[0-9]+$ ]] || { echo "error: --limit must be a non-negative integer" >&2; exit 2; }
mkdir -p "$(dirname "$OUT")"
LOG_DIR="${OUT%.jsonl}.logs"
mkdir -p "$LOG_DIR"
: > "$OUT"
declare -a CCR_FILES=()
if [[ -f "$CCR_ROOT" ]]; then
CCR_FILES=("$(readlink -f "$CCR_ROOT")")
elif [[ -d "$CCR_ROOT" ]]; then
while IFS= read -r -d '' file; do
CCR_FILES+=("$file")
done < <(find "$CCR_ROOT" -type f -name '*.ccr' -print0 | sort -z)
else
echo "error: CCR root does not exist: $CCR_ROOT" >&2
exit 2
fi
if (( LIMIT > 0 && ${#CCR_FILES[@]} > LIMIT )); then
CCR_FILES=("${CCR_FILES[@]:0:LIMIT}")
fi
if (( ${#CCR_FILES[@]} == 0 )); then
echo "error: no .ccr files found under: $CCR_ROOT" >&2
exit 2
fi
passed=0
failed=0
for ccr in "${CCR_FILES[@]}"; do
digest=$(sha256sum "$ccr" | awk '{print $1}')
log="$LOG_DIR/${digest}.log"
set +e
"$RPKI_CLIENT" -f "$ccr" >"$log" 2>&1
exit_code=$?
set -e
classification=pass
if (( exit_code != 0 )); then
classification=tool_error
elif grep -Fq "rpki-client: ${ccr}:" "$log"; then
classification=ccr_parse_error
fi
if [[ "$classification" == pass ]]; then
((passed += 1))
else
((failed += 1))
fi
python3 - "$OUT" "$ccr" "$digest" "$exit_code" "$classification" "$log" <<'PY'
import json
import sys
out, ccr, sha256, exit_code, classification, log = sys.argv[1:]
with open(out, "a", encoding="utf-8") as stream:
stream.write(json.dumps({
"ccr": ccr,
"sha256": sha256,
"rpkiClientExitCode": int(exit_code),
"classification": classification,
"log": log,
}, ensure_ascii=False, sort_keys=True) + "\n")
PY
done
echo "checked=${#CCR_FILES[@]} passed=${passed} failed=${failed} jsonl=${OUT} logs=${LOG_DIR}"
(( failed == 0 ))

View File

@ -6,6 +6,7 @@ use crate::ccr::build::{
};
use crate::ccr::encode::encode_manifest_state_payload_der;
use crate::ccr::hash::compute_state_hash;
use crate::ccr::manifest_location::select_manifest_signed_object_location_from_der;
use crate::ccr::model::{
CcrDigestAlgorithm, ManifestInstance, ManifestState, RpkiCanonicalCacheRepresentation,
};
@ -54,7 +55,10 @@ impl CcrManifestContribution {
aki: projection.manifest_ee_aki.clone(),
manifest_number_be: projection.manifest_number_be.clone(),
this_update,
locations_der: projection.manifest_sia_locations_der.clone(),
locations_der: vec![select_manifest_signed_object_location_from_der(
&projection.manifest_rsync_uri,
&projection.manifest_sia_locations_der,
)?],
subordinate_skis: projection.subordinate_skis.clone(),
})
}
@ -246,7 +250,8 @@ mod tests {
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::oid::{OID_AD_RPKI_NOTIFY, OID_AD_SIGNED_OBJECT};
use crate::data_model::rc::{AccessDescription, SubjectInfoAccess};
use crate::data_model::roa::{IpPrefix, RoaAfi};
use crate::data_model::ta::TrustAnchor;
use crate::data_model::tal::Tal;
@ -283,16 +288,35 @@ mod tests {
)
.expect("read manifest");
let manifest = ManifestObject::decode_der(&manifest_der).expect("decode manifest");
let manifest_uri = 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()
.find(|ad| {
ad.access_method_oid == OID_AD_SIGNED_OBJECT
&& ad.access_location.starts_with("rsync://")
})
.expect("manifest rsync signedObject")
.access_location
.clone(),
SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
};
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.origin_uris.push(manifest_uri.clone());
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_rsync_uri: manifest_uri.clone(),
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]
@ -312,23 +336,20 @@ mod tests {
.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::Ee(ee_sia) => vec![
crate::ccr::manifest_location::select_manifest_signed_object_location(
&manifest_uri,
&ee_sia.access_descriptions,
)
.expect("select manifest signedObject"),
],
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(),
manifest_rsync_uri: manifest_uri.clone(),
parent_manifest_rsync_uri: None,
tal_id: "apnic".to_string(),
ca_subject_name: "CN=test".to_string(),
@ -337,8 +358,8 @@ mod tests {
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(),
current_manifest_rsync_uri: manifest_uri.clone(),
current_crl_rsync_uri: format!("{manifest_uri}.crl"),
validated_manifest_meta: ValidatedManifestMeta {
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
validated_manifest_this_update: PackTime::from_utc_offset_datetime(
@ -381,7 +402,7 @@ mod tests {
related_artifacts: vec![VcirRelatedArtifact {
artifact_role: VcirArtifactRole::Manifest,
artifact_kind: VcirArtifactKind::Mft,
uri: Some("rsync://example.test/repo/current.mft".to_string()),
uri: Some(manifest_uri.clone()),
sha256: manifest_hash,
object_type: Some("mft".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,
@ -429,55 +450,6 @@ mod tests {
(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");
@ -512,4 +484,32 @@ mod tests {
let ci = crate::ccr::model::CcrContentInfo::new(accumulated_ccr);
verify_content_info(&ci).expect("verify accumulated ccr");
}
#[test]
fn accumulator_sanitizes_historical_projection_with_rpki_notify() {
let td = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(td.path()).expect("open rocksdb");
let (mut vcir, _, _, _) = sample_vcir_and_manifest(&store);
let notify_der =
crate::ccr::manifest_location::encode_access_description_der(&AccessDescription {
access_method_oid: OID_AD_RPKI_NOTIFY.to_string(),
access_location: "https://rrdp.example.test/notification.xml".to_string(),
})
.expect("encode rpkiNotify");
let expected_location = vcir.ccr_manifest_projection.manifest_sia_locations_der[0].clone();
vcir.ccr_manifest_projection
.manifest_sia_locations_der
.push(notify_der);
let mut accumulator = CcrAccumulator::new(Vec::new());
accumulator
.append_manifest_projection(&vcir.ccr_manifest_projection)
.expect("sanitize historical projection");
let contribution = accumulator
.manifests_by_hash
.values()
.next()
.expect("manifest contribution");
assert_eq!(contribution.locations_der, vec![expected_location]);
}
}

View File

@ -9,12 +9,13 @@ use crate::ccr::encode::{
encode_trust_anchor_state_payload_der,
};
use crate::ccr::hash::compute_state_hash;
use crate::ccr::manifest_location::select_manifest_signed_object_location;
use crate::ccr::model::{
AspaPayloadSet, AspaPayloadState, ManifestInstance, ManifestState, RoaPayloadSet,
RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState, TrustAnchorState,
};
use crate::data_model::manifest::ManifestObject;
use crate::data_model::rc::{AccessDescription, SubjectInfoAccess};
use crate::data_model::rc::SubjectInfoAccess;
use crate::data_model::roa::RoaAfi;
use crate::data_model::router_cert::BgpsecRouterCertificate;
use crate::data_model::ta::TrustAnchor;
@ -68,12 +69,17 @@ pub enum CcrBuildError {
#[error("manifest EE certificate SIA is not the EE form: {0}")]
ManifestEeSiaWrongVariant(String),
#[error(
"manifest EE certificate SIA location selection failed for {manifest_rsync_uri}: {detail}"
)]
ManifestSiaLocationSelection {
manifest_rsync_uri: String,
detail: String,
},
#[error("manifest child SKI is not valid lowercase hex or not 20 bytes: {0}")]
InvalidChildSki(String),
#[error("manifest AccessDescription contains unsupported accessMethod OID: {0}")]
UnsupportedAccessMethodOid(String),
#[error("manifest state encoding failed: {0}")]
ManifestEncode(String),
@ -262,11 +268,16 @@ pub fn build_manifest_state_from_vcirs_with_breakdown(
CcrBuildError::ManifestEeMissingSia(vcir.current_manifest_rsync_uri.clone())
})?;
let locations = match sia {
SubjectInfoAccess::Ee(ee_sia) => ee_sia
.access_descriptions
.iter()
.map(encode_access_description_der)
.collect::<Result<Vec<_>, _>>()?,
SubjectInfoAccess::Ee(ee_sia) => vec![
select_manifest_signed_object_location(
&vcir.current_manifest_rsync_uri,
&ee_sia.access_descriptions,
)
.map_err(|detail| CcrBuildError::ManifestSiaLocationSelection {
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
detail,
})?,
],
SubjectInfoAccess::Ca(_) => {
return Err(CcrBuildError::ManifestEeSiaWrongVariant(
vcir.current_manifest_rsync_uri.clone(),
@ -353,45 +364,6 @@ fn collect_subordinate_ski_bytes(
Ok(skis.into_iter().collect())
}
fn encode_access_description_der(ad: &AccessDescription) -> Result<Vec<u8>, CcrBuildError> {
let oid = encode_oid_from_string(&ad.access_method_oid)?;
let uri = encode_tlv(0x86, ad.access_location.as_bytes().to_vec());
Ok(encode_sequence(&[oid, uri]))
}
fn encode_oid_from_string(oid: &str) -> Result<Vec<u8>, CcrBuildError> {
let arcs = oid
.split('.')
.map(|part| {
part.parse::<u64>()
.map_err(|_| CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()))
})
.collect::<Result<Vec<_>, _>>()?;
if arcs.len() < 2 {
return Err(CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()));
}
if arcs[0] > 2 || (arcs[0] < 2 && arcs[1] >= 40) {
return Err(CcrBuildError::UnsupportedAccessMethodOid(oid.to_string()));
}
let mut body = Vec::new();
body.push((arcs[0] * 40 + arcs[1]) as u8);
for arc in &arcs[2..] {
encode_base128(*arc, &mut body);
}
Ok(encode_tlv(0x06, body))
}
fn encode_base128(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);
}
pub fn build_router_key_state(
router_certs: &[BgpsecRouterCertificate],
) -> Result<RouterKeyState, CcrBuildError> {
@ -650,17 +622,33 @@ mod tests {
}
}
fn sample_manifest_vcir(
manifest_uri: &str,
manifest_der: &[u8],
child_ski_hex: &str,
) -> ValidatedCaInstanceResult {
fn sample_manifest_vcir(manifest_der: &[u8], child_ski_hex: &str) -> ValidatedCaInstanceResult {
let manifest = ManifestObject::decode_der(manifest_der).expect("decode manifest fixture");
let manifest_uri = 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()
.find(|ad| {
ad.access_method_oid == crate::data_model::oid::OID_AD_SIGNED_OBJECT
&& ad.access_location.starts_with("rsync://")
})
.expect("manifest rsync signedObject")
.access_location
.clone(),
SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
};
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_rsync_uri: manifest_uri.clone(),
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]
@ -680,18 +668,19 @@ mod tests {
.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::Ee(ee_sia) => vec![
select_manifest_signed_object_location(
&manifest_uri,
&ee_sia.access_descriptions,
)
.expect("select manifest signedObject"),
],
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(),
manifest_rsync_uri: manifest_uri.clone(),
parent_manifest_rsync_uri: None,
tal_id: "test-tal".to_string(),
ca_subject_name: "CN=test".to_string(),
@ -700,7 +689,7 @@ mod tests {
last_successful_validation_time: crate::storage::PackTime::from_utc_offset_datetime(
now,
),
current_manifest_rsync_uri: manifest_uri.to_string(),
current_manifest_rsync_uri: manifest_uri.clone(),
current_crl_rsync_uri: format!("{manifest_uri}.crl"),
validated_manifest_meta: crate::storage::ValidatedManifestMeta {
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
@ -736,7 +725,7 @@ mod tests {
related_artifacts: vec![crate::storage::VcirRelatedArtifact {
artifact_role: crate::storage::VcirArtifactRole::Manifest,
artifact_kind: crate::storage::VcirArtifactKind::Mft,
uri: Some(manifest_uri.to_string()),
uri: Some(manifest_uri.clone()),
sha256: hash,
object_type: Some("mft".to_string()),
validation_status: crate::storage::VcirArtifactValidationStatus::Accepted,
@ -766,10 +755,8 @@ mod tests {
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft",
))
.expect("read manifest b");
let vcir_a =
sample_manifest_vcir("rsync://example.test/a.mft", &manifest_a, &"33".repeat(20));
let vcir_b =
sample_manifest_vcir("rsync://example.test/b.mft", &manifest_b, &"44".repeat(20));
let vcir_a = sample_manifest_vcir(&manifest_a, &"33".repeat(20));
let vcir_b = sample_manifest_vcir(&manifest_b, &"44".repeat(20));
let store_dir = tempfile::tempdir().expect("tempdir");
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
for (vcir, bytes) in [(&vcir_a, &manifest_a), (&vcir_b, &manifest_b)] {

View File

@ -471,3 +471,71 @@ fn decode_big_unsigned(bytes: &[u8]) -> Result<BigUnsigned, CcrDecodeError> {
};
Ok(BigUnsigned { bytes_be })
}
#[cfg(all(test, feature = "full"))]
mod tests {
use super::*;
use crate::ccr::encode::{encode_content_info, encode_manifest_state_payload_der};
use crate::ccr::hash::compute_state_hash;
use crate::ccr::manifest_location::encode_access_description_der;
use crate::ccr::model::{
CcrContentInfo, CcrDigestAlgorithm, ManifestState, RpkiCanonicalCacheRepresentation,
};
use crate::data_model::oid::{OID_AD_RPKI_NOTIFY, OID_AD_SIGNED_OBJECT};
use crate::data_model::rc::AccessDescription;
#[test]
fn generic_decoder_preserves_multiple_manifest_locations() {
let signed_object = encode_access_description_der(&AccessDescription {
access_method_oid: OID_AD_SIGNED_OBJECT.to_string(),
access_location: "rsync://example.test/repository/current.mft".to_string(),
})
.expect("encode signedObject");
let rpki_notify = encode_access_description_der(&AccessDescription {
access_method_oid: OID_AD_RPKI_NOTIFY.to_string(),
access_location: "https://rrdp.example.test/notification.xml".to_string(),
})
.expect("encode rpkiNotify");
let manifest = ManifestInstance {
hash: vec![0x11; 32],
size: 1024,
aki: vec![0x22; 20],
manifest_number: BigUnsigned { bytes_be: vec![1] },
this_update: time::OffsetDateTime::parse(
"2026-07-20T00:00:00Z",
&time::format_description::well_known::Rfc3339,
)
.expect("parse time"),
locations: vec![signed_object, rpki_notify],
subordinates: Vec::new(),
};
let manifest_payload = encode_manifest_state_payload_der(&[manifest.clone()])
.expect("encode manifest payload");
let content = CcrContentInfo::new(RpkiCanonicalCacheRepresentation {
version: 0,
hash_alg: CcrDigestAlgorithm::Sha256,
produced_at: time::OffsetDateTime::parse(
"2026-07-20T00:00:00Z",
&time::format_description::well_known::Rfc3339,
)
.expect("parse time"),
mfts: Some(ManifestState {
mis: vec![manifest],
most_recent_update: time::OffsetDateTime::parse(
"2026-07-20T00:00:00Z",
&time::format_description::well_known::Rfc3339,
)
.expect("parse time"),
hash: compute_state_hash(&manifest_payload),
}),
vrps: None,
vaps: None,
tas: None,
rks: None,
});
let encoded = encode_content_info(&content).expect("encode CCR");
let decoded = decode_content_info(&encoded).expect("decode CCR");
assert_eq!(decoded.content.mfts.unwrap().mis[0].locations.len(), 2);
}
}

View File

@ -124,6 +124,8 @@ mod tests {
use super::*;
use crate::ccr::decode::decode_content_info;
use crate::data_model::manifest::ManifestObject;
use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
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;
@ -149,15 +151,34 @@ mod tests {
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_uri = 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()
.find(|ad| {
ad.access_method_oid == OID_AD_SIGNED_OBJECT
&& ad.access_location.starts_with("rsync://")
})
.expect("manifest rsync signedObject")
.access_location
.clone(),
SubjectInfoAccess::Ca(_) => panic!("manifest EE SIA should not be CA variant"),
};
let hash = hex::encode(sha2::Sha256::digest(&manifest_der));
let mut raw = RawByHashEntry::from_bytes(hash.clone(), manifest_der.clone());
raw.origin_uris
.push("rsync://example.test/repo/current.mft".to_string());
raw.origin_uris.push(manifest_uri.clone());
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_rsync_uri: manifest_uri.clone(),
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]
@ -169,14 +190,27 @@ mod tests {
.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',
]],
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) => vec![
crate::ccr::manifest_location::select_manifest_signed_object_location(
&manifest_uri,
&ee_sia.access_descriptions,
)
.expect("select manifest signedObject"),
],
SubjectInfoAccess::Ca(_) => panic!("manifest EE SIA should not be CA variant"),
},
subordinate_skis: vec![vec![0x33; 20]],
};
ValidatedCaInstanceResult {
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
manifest_rsync_uri: manifest_uri.clone(),
parent_manifest_rsync_uri: None,
tal_id: "apnic".to_string(),
ca_subject_name: "CN=test".to_string(),
@ -185,7 +219,7 @@ mod tests {
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_manifest_rsync_uri: manifest_uri.clone(),
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(),
@ -229,7 +263,7 @@ mod tests {
related_artifacts: vec![VcirRelatedArtifact {
artifact_role: VcirArtifactRole::Manifest,
artifact_kind: VcirArtifactKind::Mft,
uri: Some("rsync://example.test/repo/current.mft".to_string()),
uri: Some(manifest_uri),
sha256: hash,
object_type: Some("mft".to_string()),
validation_status: VcirArtifactValidationStatus::Accepted,

View File

@ -0,0 +1,424 @@
use crate::data_model::common::DerReader;
use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
use crate::data_model::rc::AccessDescription;
pub(crate) fn select_manifest_signed_object_location(
manifest_rsync_uri: &str,
access_descriptions: &[AccessDescription],
) -> Result<Vec<u8>, String> {
let matching = access_descriptions
.iter()
.filter(|access_description| {
access_description.access_method_oid == OID_AD_SIGNED_OBJECT
&& access_description.access_location == manifest_rsync_uri
})
.collect::<Vec<_>>();
let access_description = expect_single_matching_location(
manifest_rsync_uri,
matching.len(),
"parsed Manifest EE SIA",
)?;
encode_access_description_der(matching[access_description])
}
pub(crate) fn select_manifest_signed_object_location_from_der(
manifest_rsync_uri: &str,
locations_der: &[Vec<u8>],
) -> Result<Vec<u8>, String> {
let matching = locations_der
.iter()
.filter_map(
|location_der| match decode_access_description_der(location_der) {
Ok(access_description)
if access_description.access_method_oid == OID_AD_SIGNED_OBJECT
&& access_description.access_location == manifest_rsync_uri =>
{
Some(Ok(location_der.clone()))
}
Ok(_) => None,
Err(detail) => Some(Err(detail)),
},
)
.collect::<Result<Vec<_>, _>>()?;
let index = expect_single_matching_location(
manifest_rsync_uri,
matching.len(),
"persisted VCIR CCR projection",
)?;
Ok(matching[index].clone())
}
pub(crate) fn encode_access_description_der(
access_description: &AccessDescription,
) -> Result<Vec<u8>, String> {
let oid = encode_oid_der(&access_description.access_method_oid)?;
let uri = encode_tlv(0x86, access_description.access_location.as_bytes().to_vec());
Ok(encode_sequence(&[oid, uri]))
}
fn expect_single_matching_location(
manifest_rsync_uri: &str,
matching_count: usize,
source: &str,
) -> Result<usize, String> {
if matching_count == 1 {
return Ok(0);
}
Err(format!(
"{source} contains {matching_count} id-ad-signedObject locations matching manifest URI {manifest_rsync_uri}; expected exactly one"
))
}
fn decode_access_description_der(der: &[u8]) -> Result<AccessDescription, String> {
let mut top = DerReader::new(der);
let mut sequence = top.take_sequence()?;
if !top.is_empty() {
return Err("trailing bytes after AccessDescription".to_string());
}
let access_method_oid = decode_oid_der(sequence.take_tag(0x06)?)?;
let access_location = std::str::from_utf8(sequence.take_tag(0x86)?)
.map_err(|error| format!("AccessDescription URI is not UTF-8: {error}"))?
.to_string();
if !sequence.is_empty() {
return Err("trailing fields in AccessDescription".to_string());
}
Ok(AccessDescription {
access_method_oid,
access_location,
})
}
fn decode_oid_der(value: &[u8]) -> Result<String, String> {
let mut offset = 0usize;
let first = decode_base128(value, &mut offset)?;
let (first_arc, second_arc) = match first {
0..=39 => (0, first),
40..=79 => (1, first - 40),
value => (2, value - 80),
};
let mut arcs = vec![first_arc, second_arc];
while offset < value.len() {
arcs.push(decode_base128(value, &mut offset)?);
}
Ok(arcs
.into_iter()
.map(|arc| arc.to_string())
.collect::<Vec<_>>()
.join("."))
}
fn decode_base128(value: &[u8], offset: &mut usize) -> Result<u64, String> {
let first = *value
.get(*offset)
.ok_or_else(|| "truncated OBJECT IDENTIFIER".to_string())?;
if first == 0x80 {
return Err("non-minimal OBJECT IDENTIFIER base-128 encoding".to_string());
}
let mut out = 0u64;
loop {
let byte = *value
.get(*offset)
.ok_or_else(|| "truncated OBJECT IDENTIFIER".to_string())?;
*offset += 1;
out = out
.checked_shl(7)
.ok_or_else(|| "OBJECT IDENTIFIER arc overflows u64".to_string())?
.checked_add((byte & 0x7f) as u64)
.ok_or_else(|| "OBJECT IDENTIFIER arc overflows u64".to_string())?;
if byte & 0x80 == 0 {
return Ok(out);
}
}
}
fn encode_oid_der(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 || arcs[0] > 2 || (arcs[0] < 2 && arcs[1] >= 40) {
return Err(format!("unsupported accessMethod OID: {oid}"));
}
let mut body = Vec::new();
encode_base128(
arcs[0]
.checked_mul(40)
.and_then(|value| value.checked_add(arcs[1]))
.ok_or_else(|| format!("unsupported accessMethod OID: {oid}"))?,
&mut body,
);
for arc in &arcs[2..] {
encode_base128(*arc, &mut body);
}
Ok(encode_tlv(0x06, body))
}
fn encode_base128(mut value: u64, out: &mut Vec<u8>) {
let mut encoded = vec![(value & 0x7f) as u8];
value >>= 7;
while value > 0 {
encoded.push(((value & 0x7f) as u8) | 0x80);
value >>= 7;
}
encoded.reverse();
out.extend_from_slice(&encoded);
}
fn encode_sequence(elements: &[Vec<u8>]) -> Vec<u8> {
let total_len = elements.iter().map(Vec::len).sum();
let mut value = Vec::with_capacity(total_len);
for element in elements {
value.extend_from_slice(element);
}
encode_tlv(0x30, value)
}
fn encode_tlv(tag: u8, value: Vec<u8>) -> Vec<u8> {
let mut out = Vec::with_capacity(1 + 9 + value.len());
out.push(tag);
encode_length(value.len(), &mut out);
out.extend_from_slice(&value);
out
}
fn encode_length(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);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::data_model::oid::OID_AD_RPKI_NOTIFY;
const MANIFEST_URI: &str = "rsync://example.test/repo/manifest.mft";
fn access_description(access_method_oid: &str, access_location: &str) -> AccessDescription {
AccessDescription {
access_method_oid: access_method_oid.to_string(),
access_location: access_location.to_string(),
}
}
#[test]
fn selects_matching_signed_object_and_excludes_rpki_notify() {
let signed_object = access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
let selected = select_manifest_signed_object_location(
MANIFEST_URI,
&[
signed_object.clone(),
access_description(
OID_AD_RPKI_NOTIFY,
"https://rrdp.example.test/notification.xml",
),
],
)
.expect("select signed object");
assert_eq!(
selected,
encode_access_description_der(&signed_object).unwrap()
);
}
#[test]
fn selects_only_the_signed_object_matching_manifest_uri() {
let expected = access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
let selected = select_manifest_signed_object_location(
MANIFEST_URI,
&[
access_description(
OID_AD_SIGNED_OBJECT,
"https://backup.example.test/manifest.mft",
),
expected.clone(),
],
)
.expect("select matching signed object");
assert_eq!(selected, encode_access_description_der(&expected).unwrap());
}
#[test]
fn rejects_missing_or_duplicate_matching_signed_object() {
let missing = select_manifest_signed_object_location(
MANIFEST_URI,
&[access_description(
OID_AD_RPKI_NOTIFY,
"https://rrdp.example.test/notification.xml",
)],
)
.expect_err("missing signed object must fail");
assert!(missing.contains("contains 0"), "{missing}");
let duplicate = select_manifest_signed_object_location(
MANIFEST_URI,
&[
access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI),
access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI),
],
)
.expect_err("duplicate signed object must fail");
assert!(duplicate.contains("contains 2"), "{duplicate}");
}
#[test]
fn selects_matching_signed_object_from_historical_projection() {
let signed_object = access_description(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
let signed_object_der = encode_access_description_der(&signed_object).unwrap();
let notify_der = encode_access_description_der(&access_description(
OID_AD_RPKI_NOTIFY,
"https://rrdp.example.test/notification.xml",
))
.unwrap();
let selected = select_manifest_signed_object_location_from_der(
MANIFEST_URI,
&[notify_der, signed_object_der.clone()],
)
.expect("select historical signed object");
assert_eq!(selected, signed_object_der);
}
#[test]
fn rejects_malformed_historical_projection() {
let error = select_manifest_signed_object_location_from_der(
MANIFEST_URI,
&[vec![0x30, 0x01, 0x06]],
)
.expect_err("malformed historical projection must fail");
assert!(error.contains("truncated DER"), "{error}");
}
#[test]
fn access_description_der_codec_covers_long_and_invalid_forms() {
let long = access_description(
"2.999.200.1",
&format!("rsync://example.test/repo/{}", "x".repeat(160)),
);
let encoded = encode_access_description_der(&long).expect("encode long access description");
assert_eq!(
decode_access_description_der(&encoded).expect("decode long access description"),
long
);
let mut trailing = encoded.clone();
trailing.push(0);
assert!(
decode_access_description_der(&trailing)
.expect_err("trailing bytes must fail")
.contains("trailing bytes")
);
let oid = encode_oid_der(OID_AD_SIGNED_OBJECT).expect("encode signedObject OID");
let uri = encode_tlv(0x86, MANIFEST_URI.as_bytes().to_vec());
assert!(
decode_access_description_der(&encode_sequence(&[
oid.clone(),
uri.clone(),
encode_tlv(0x05, Vec::new()),
]))
.expect_err("trailing field must fail")
.contains("trailing fields")
);
assert!(
decode_access_description_der(&encode_sequence(&[oid, encode_tlv(0x86, vec![0xff]),]))
.expect_err("non-UTF8 URI must fail")
.contains("not UTF-8")
);
assert!(
decode_access_description_der(&encode_sequence(&[
encode_tlv(0x06, vec![0x80, 0x00]),
uri.clone(),
]))
.expect_err("non-minimal OID must fail")
.contains("non-minimal")
);
assert!(
decode_access_description_der(&encode_sequence(&[encode_tlv(0x06, vec![0x81]), uri,]))
.expect_err("truncated OID must fail")
.contains("truncated OBJECT IDENTIFIER")
);
assert!(
encode_access_description_der(&access_description("3.1", MANIFEST_URI))
.expect_err("invalid OID must fail")
.contains("unsupported accessMethod OID")
);
}
#[test]
fn access_description_der_codec_reports_malformed_selector_inputs() {
let malformed_sequence = decode_access_description_der(&[0x31, 0x00])
.expect_err("non-sequence AccessDescription must fail");
assert!(!malformed_sequence.is_empty());
let missing_method = decode_access_description_der(&encode_sequence(&[encode_tlv(
0x86,
MANIFEST_URI.as_bytes().to_vec(),
)]))
.expect_err("missing accessMethod must fail");
assert!(!missing_method.is_empty());
let oid = encode_oid_der(OID_AD_SIGNED_OBJECT).expect("encode signedObject OID");
let missing_location = decode_access_description_der(&encode_sequence(&[oid.clone()]))
.expect_err("missing accessLocation must fail");
assert!(!missing_location.is_empty());
let wrong_location_tag = decode_access_description_der(&encode_sequence(&[
oid.clone(),
encode_tlv(0x04, MANIFEST_URI.as_bytes().to_vec()),
]))
.expect_err("wrong accessLocation tag must fail");
assert!(!wrong_location_tag.is_empty());
let empty_oid = decode_access_description_der(&encode_sequence(&[
encode_tlv(0x06, Vec::new()),
encode_tlv(0x86, MANIFEST_URI.as_bytes().to_vec()),
]))
.expect_err("empty OID must fail");
assert!(!empty_oid.is_empty());
let first_arc_zero = decode_access_description_der(&encode_sequence(&[
encode_tlv(0x06, vec![0x01, 0x02]),
encode_tlv(0x86, MANIFEST_URI.as_bytes().to_vec()),
]))
.expect("decode first OID arc zero");
assert_eq!(first_arc_zero.access_method_oid, "0.1.2");
assert_eq!(first_arc_zero.access_location, MANIFEST_URI);
let non_numeric =
encode_access_description_der(&access_description("no.such.oid", MANIFEST_URI))
.expect_err("non-numeric OID must fail");
assert!(!non_numeric.is_empty());
let too_short = encode_access_description_der(&access_description("1", MANIFEST_URI))
.expect_err("OID without second arc must fail");
assert!(!too_short.is_empty());
let invalid_second_arc =
encode_access_description_der(&access_description("1.40", MANIFEST_URI))
.expect_err("invalid second OID arc must fail");
assert!(!invalid_second_arc.is_empty());
let overflowing_first_subidentifier = encode_access_description_der(&access_description(
"2.18446744073709551615",
MANIFEST_URI,
))
.expect_err("overflowing first OID subidentifier must fail");
assert!(!overflowing_first_subidentifier.is_empty());
}
}

View File

@ -10,6 +10,8 @@ pub mod encode;
#[cfg(feature = "full")]
pub mod export;
pub mod hash;
#[cfg(feature = "full")]
pub(crate) mod manifest_location;
pub mod model;
pub mod state_digest;
#[cfg(feature = "full")]

View File

@ -1,4 +1,6 @@
use super::*;
use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
use crate::data_model::rc::AccessDescription;
use crate::memory_telemetry::{
MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot,
};
@ -1722,8 +1724,9 @@ fn sample_cli_ccr_accumulator() -> CcrAccumulator {
)
.expect("discover root");
let mut accumulator = CcrAccumulator::new(vec![discovery.trust_anchor.clone()]);
let manifest_uri = "rsync://example.test/repo/current.mft".to_string();
let projection = crate::storage::VcirCcrManifestProjection {
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
manifest_rsync_uri: manifest_uri.clone(),
manifest_sha256: vec![0x44; 32],
manifest_size: 2048,
manifest_ee_aki: vec![0x55; 20],
@ -1731,10 +1734,13 @@ fn sample_cli_ccr_accumulator() -> CcrAccumulator {
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',
]],
manifest_sia_locations_der: vec![
crate::ccr::manifest_location::encode_access_description_der(&AccessDescription {
access_method_oid: OID_AD_SIGNED_OBJECT.to_string(),
access_location: manifest_uri,
})
.expect("encode signedObject"),
],
subordinate_skis: vec![vec![0x33; 20]],
};
accumulator

View File

@ -1,5 +1,3 @@
mod vcir_der;
use crate::analysis::timing::TimingHandle;
use crate::audit::{
AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit,
@ -69,7 +67,7 @@ use base64::Engine as _;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
use vcir_der::encode_access_description_der_for_vcir_ccr_projection;
use crate::ccr::manifest_location::select_manifest_signed_object_location;
const PUBLICATION_POINT_CACHE_CHILD_RESTORE_PARALLEL_MIN_CHILDREN: usize = 256;
const PUBLICATION_POINT_CACHE_CHILD_RESTORE_MAX_WORKERS: usize = 16;
@ -5677,11 +5675,10 @@ fn build_vcir_ccr_manifest_projection_from_fresh(
.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::Ee(ee_sia) => vec![select_manifest_signed_object_location(
&ca.manifest_rsync_uri,
&ee_sia.access_descriptions,
)?],
SubjectInfoAccess::Ca(_) => {
return Err(
"manifest EE certificate Subject Information Access has CA variant".to_string(),

View File

@ -1,5 +1,8 @@
use super::*;
use crate::data_model::rc::{AsIdOrRange, AsIdentifierChoice, AsResourceSet, ResourceCertificate};
use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
use crate::data_model::rc::{
AccessDescription, AsIdOrRange, AsIdentifierChoice, AsResourceSet, ResourceCertificate,
};
use crate::data_model::roa::RoaAfi;
use crate::fetch::rsync::LocalDirRsyncFetcher;
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
@ -603,10 +606,13 @@ fn sample_vcir_for_projection(
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',
]],
manifest_sia_locations_der: vec![
crate::ccr::manifest_location::encode_access_description_der(&AccessDescription {
access_method_oid: OID_AD_SIGNED_OBJECT.to_string(),
access_location: manifest_uri.clone(),
})
.expect("encode signedObject"),
],
subordinate_skis: vec![vec![0x33; 20]],
};
ValidatedCaInstanceResult {
@ -1252,12 +1258,13 @@ fn build_vcir_ccr_manifest_projection_from_fresh_real_snapshot_matches_manifest_
.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::Ee(ee_sia) => vec![
crate::ccr::manifest_location::select_manifest_signed_object_location(
&pack.manifest_rsync_uri,
&ee_sia.access_descriptions,
)
.expect("select manifest signedObject"),
],
SubjectInfoAccess::Ca(_) => panic!("manifest ee SIA should not be CA variant"),
};

View File

@ -1,78 +0,0 @@
use crate::data_model::rc::AccessDescription;
pub(super) 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);
}

View File

@ -0,0 +1,139 @@
use rpki::ccr::{CcrAccumulator, build_roa_payload_state};
use rpki::data_model::roa::{IpPrefix, RoaAfi};
use rpki::storage::{PackTime, VcirCcrManifestProjection};
use rpki::validation::objects::Vrp;
const MANIFEST_URI: &str = "rsync://example.test/repo/current.mft";
const OID_AD_SIGNED_OBJECT: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x0b];
const OID_AD_RPKI_NOTIFY: &[u8] = &[0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x0d];
fn der_length(len: usize) -> Vec<u8> {
if len < 0x80 {
return vec![len as u8];
}
let mut bytes = len.to_be_bytes().to_vec();
let first = bytes
.iter()
.position(|byte| *byte != 0)
.expect("non-zero length");
bytes.drain(..first);
let mut out = vec![0x80 | bytes.len() as u8];
out.extend_from_slice(&bytes);
out
}
fn der_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut out = vec![tag];
out.extend(der_length(value.len()));
out.extend_from_slice(value);
out
}
fn access_description_der(access_method_oid: &[u8], uri: &str) -> Vec<u8> {
let oid = der_tlv(0x06, access_method_oid);
let location = der_tlv(0x86, uri.as_bytes());
let mut body = oid;
body.extend(location);
der_tlv(0x30, &body)
}
fn projection(locations: Vec<Vec<u8>>) -> VcirCcrManifestProjection {
VcirCcrManifestProjection {
manifest_rsync_uri: MANIFEST_URI.to_string(),
manifest_sha256: vec![0x11; 32],
manifest_size: 1000,
manifest_ee_aki: vec![0x22; 20],
manifest_number_be: vec![1],
manifest_this_update: PackTime::from_utc_offset_datetime(time::OffsetDateTime::now_utc()),
manifest_sia_locations_der: locations,
subordinate_skis: vec![vec![0x33; 20]],
}
}
#[test]
fn historical_projection_normalizes_locations_and_accounts_memory() {
let signed_object = access_description_der(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
let notify = access_description_der(
OID_AD_RPKI_NOTIFY,
"https://rrdp.example.test/notification.xml",
);
let projection = projection(vec![notify, signed_object]);
let mut accumulator = CcrAccumulator::new(Vec::new());
accumulator
.append_manifest_projection(&projection)
.expect("append normalized historical projection");
accumulator
.append_manifest_projection(&projection)
.expect("duplicate matching projection is idempotent");
assert_eq!(accumulator.manifest_count(), 1);
let stats = accumulator.memory_stats();
assert_eq!(stats.manifest_count, 1);
assert_eq!(stats.locations_der_count, 1);
assert_eq!(stats.subordinate_ski_count, 1);
assert!(stats.estimated_heap_bytes > 0);
assert!(stats.btree_entry_shallow_bytes > 0);
let mut conflicting = projection;
conflicting.subordinate_skis.push(vec![0x44; 20]);
let error = accumulator
.append_manifest_projection(&conflicting)
.expect_err("same hash with different projection must fail");
assert!(error.contains("duplicate manifest hash"), "{error}");
}
#[test]
fn historical_projection_rejects_invalid_location_sets() {
let mut accumulator = CcrAccumulator::new(Vec::new());
let empty_error = accumulator
.append_manifest_projection(&projection(Vec::new()))
.expect_err("empty location set must fail");
assert!(empty_error.contains("contains 0"), "{empty_error}");
let signed_object = access_description_der(OID_AD_SIGNED_OBJECT, MANIFEST_URI);
let duplicate_error = accumulator
.append_manifest_projection(&projection(vec![signed_object.clone(), signed_object]))
.expect_err("duplicate matching locations must fail");
assert!(duplicate_error.contains("contains 2"), "{duplicate_error}");
let malformed_error = accumulator
.append_manifest_projection(&projection(vec![vec![0x30, 0x01, 0x06]]))
.expect_err("malformed location DER must fail");
assert!(
malformed_error.contains("truncated DER"),
"{malformed_error}"
);
}
#[test]
fn roa_payload_builder_encodes_large_non_byte_aligned_family() {
let mut vrps = Vec::new();
for octet in 0..=255 {
vrps.push(Vrp {
asn: 64_496,
prefix: IpPrefix {
afi: RoaAfi::Ipv4,
prefix_len: 24,
addr: [10, octet, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
},
max_length: 24,
});
}
vrps.push(Vrp {
asn: 64_496,
prefix: IpPrefix {
afi: RoaAfi::Ipv4,
prefix_len: 13,
addr: [10, 0x1f, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
},
max_length: 24,
});
let state = build_roa_payload_state(&vrps).expect("build ROA payload state");
assert_eq!(state.rps.len(), 1);
assert_eq!(state.rps[0].as_id, 64_496);
assert_eq!(state.rps[0].ip_addr_blocks.len(), 1);
assert!(state.rps[0].ip_addr_blocks[0].len() > 128);
}