20260720 规范化 CCR Manifest location 导出
This commit is contained in:
parent
06f4b086b4
commit
daf0764cd0
107
scripts/ccr/check_rpki_client_ccr.sh
Executable file
107
scripts/ccr/check_rpki_client_ccr.sh
Executable 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 ))
|
||||
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
121
src/ccr/build.rs
121
src/ccr/build.rs
@ -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)] {
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
424
src/ccr/manifest_location.rs
Normal file
424
src/ccr/manifest_location.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
@ -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")]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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"),
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
139
tests/test_ccr_manifest_location_interop.rs
Normal file
139
tests/test_ccr_manifest_location_interop.rs
Normal 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);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user