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::encode::encode_manifest_state_payload_der;
|
||||||
use crate::ccr::hash::compute_state_hash;
|
use crate::ccr::hash::compute_state_hash;
|
||||||
|
use crate::ccr::manifest_location::select_manifest_signed_object_location_from_der;
|
||||||
use crate::ccr::model::{
|
use crate::ccr::model::{
|
||||||
CcrDigestAlgorithm, ManifestInstance, ManifestState, RpkiCanonicalCacheRepresentation,
|
CcrDigestAlgorithm, ManifestInstance, ManifestState, RpkiCanonicalCacheRepresentation,
|
||||||
};
|
};
|
||||||
@ -54,7 +55,10 @@ impl CcrManifestContribution {
|
|||||||
aki: projection.manifest_ee_aki.clone(),
|
aki: projection.manifest_ee_aki.clone(),
|
||||||
manifest_number_be: projection.manifest_number_be.clone(),
|
manifest_number_be: projection.manifest_number_be.clone(),
|
||||||
this_update,
|
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(),
|
subordinate_skis: projection.subordinate_skis.clone(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -246,7 +250,8 @@ mod tests {
|
|||||||
use crate::ccr::export::build_ccr_from_run;
|
use crate::ccr::export::build_ccr_from_run;
|
||||||
use crate::ccr::verify::verify_content_info;
|
use crate::ccr::verify::verify_content_info;
|
||||||
use crate::data_model::manifest::ManifestObject;
|
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::roa::{IpPrefix, RoaAfi};
|
||||||
use crate::data_model::ta::TrustAnchor;
|
use crate::data_model::ta::TrustAnchor;
|
||||||
use crate::data_model::tal::Tal;
|
use crate::data_model::tal::Tal;
|
||||||
@ -283,16 +288,35 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.expect("read manifest");
|
.expect("read manifest");
|
||||||
let manifest = ManifestObject::decode_der(&manifest_der).expect("decode 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 manifest_hash = hex::encode(sha2::Sha256::digest(&manifest_der));
|
||||||
let mut raw = RawByHashEntry::from_bytes(manifest_hash.clone(), manifest_der.clone());
|
let mut raw = RawByHashEntry::from_bytes(manifest_hash.clone(), manifest_der.clone());
|
||||||
raw.origin_uris
|
raw.origin_uris.push(manifest_uri.clone());
|
||||||
.push("rsync://example.test/repo/current.mft".to_string());
|
|
||||||
raw.object_type = Some("mft".to_string());
|
raw.object_type = Some("mft".to_string());
|
||||||
raw.encoding = Some("der".to_string());
|
raw.encoding = Some("der".to_string());
|
||||||
store.put_raw_by_hash_entry(&raw).expect("put raw");
|
store.put_raw_by_hash_entry(&raw).expect("put raw");
|
||||||
|
|
||||||
let projection = VcirCcrManifestProjection {
|
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_sha256: sha2::Sha256::digest(&manifest_der).to_vec(),
|
||||||
manifest_size: manifest_der.len() as u64,
|
manifest_size: manifest_der.len() as u64,
|
||||||
manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
|
manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
|
||||||
@ -312,23 +336,20 @@ mod tests {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("manifest sia")
|
.expect("manifest sia")
|
||||||
{
|
{
|
||||||
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
SubjectInfoAccess::Ee(ee_sia) => vec![
|
||||||
.access_descriptions
|
crate::ccr::manifest_location::select_manifest_signed_object_location(
|
||||||
.iter()
|
&manifest_uri,
|
||||||
.map(|ad| {
|
&ee_sia.access_descriptions,
|
||||||
let oid = encode_oid_for_test(&ad.access_method_oid)
|
)
|
||||||
.expect("encode access method oid");
|
.expect("select manifest signedObject"),
|
||||||
let uri = encode_tlv_for_test(0x86, ad.access_location.as_bytes().to_vec());
|
],
|
||||||
encode_sequence_for_test(&[oid, uri])
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
|
SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
|
||||||
},
|
},
|
||||||
subordinate_skis: vec![vec![0x33; 20]],
|
subordinate_skis: vec![vec![0x33; 20]],
|
||||||
};
|
};
|
||||||
|
|
||||||
let vcir = ValidatedCaInstanceResult {
|
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,
|
parent_manifest_rsync_uri: None,
|
||||||
tal_id: "apnic".to_string(),
|
tal_id: "apnic".to_string(),
|
||||||
ca_subject_name: "CN=test".to_string(),
|
ca_subject_name: "CN=test".to_string(),
|
||||||
@ -337,8 +358,8 @@ mod tests {
|
|||||||
last_successful_validation_time: PackTime::from_utc_offset_datetime(
|
last_successful_validation_time: PackTime::from_utc_offset_datetime(
|
||||||
manifest.manifest.this_update,
|
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(),
|
current_crl_rsync_uri: format!("{manifest_uri}.crl"),
|
||||||
validated_manifest_meta: ValidatedManifestMeta {
|
validated_manifest_meta: ValidatedManifestMeta {
|
||||||
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
|
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||||
validated_manifest_this_update: PackTime::from_utc_offset_datetime(
|
validated_manifest_this_update: PackTime::from_utc_offset_datetime(
|
||||||
@ -381,7 +402,7 @@ mod tests {
|
|||||||
related_artifacts: vec![VcirRelatedArtifact {
|
related_artifacts: vec![VcirRelatedArtifact {
|
||||||
artifact_role: VcirArtifactRole::Manifest,
|
artifact_role: VcirArtifactRole::Manifest,
|
||||||
artifact_kind: VcirArtifactKind::Mft,
|
artifact_kind: VcirArtifactKind::Mft,
|
||||||
uri: Some("rsync://example.test/repo/current.mft".to_string()),
|
uri: Some(manifest_uri.clone()),
|
||||||
sha256: manifest_hash,
|
sha256: manifest_hash,
|
||||||
object_type: Some("mft".to_string()),
|
object_type: Some("mft".to_string()),
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
||||||
@ -429,55 +450,6 @@ mod tests {
|
|||||||
(vcir, vrps, aspas, router_keys)
|
(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]
|
#[test]
|
||||||
fn accumulator_finish_matches_builder_on_fresh_vcir_inputs() {
|
fn accumulator_finish_matches_builder_on_fresh_vcir_inputs() {
|
||||||
let td = tempfile::tempdir().expect("tempdir");
|
let td = tempfile::tempdir().expect("tempdir");
|
||||||
@ -512,4 +484,32 @@ mod tests {
|
|||||||
let ci = crate::ccr::model::CcrContentInfo::new(accumulated_ccr);
|
let ci = crate::ccr::model::CcrContentInfo::new(accumulated_ccr);
|
||||||
verify_content_info(&ci).expect("verify 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,
|
encode_trust_anchor_state_payload_der,
|
||||||
};
|
};
|
||||||
use crate::ccr::hash::compute_state_hash;
|
use crate::ccr::hash::compute_state_hash;
|
||||||
|
use crate::ccr::manifest_location::select_manifest_signed_object_location;
|
||||||
use crate::ccr::model::{
|
use crate::ccr::model::{
|
||||||
AspaPayloadSet, AspaPayloadState, ManifestInstance, ManifestState, RoaPayloadSet,
|
AspaPayloadSet, AspaPayloadState, ManifestInstance, ManifestState, RoaPayloadSet,
|
||||||
RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState, TrustAnchorState,
|
RoaPayloadState, RouterKey, RouterKeySet, RouterKeyState, TrustAnchorState,
|
||||||
};
|
};
|
||||||
use crate::data_model::manifest::ManifestObject;
|
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::roa::RoaAfi;
|
||||||
use crate::data_model::router_cert::BgpsecRouterCertificate;
|
use crate::data_model::router_cert::BgpsecRouterCertificate;
|
||||||
use crate::data_model::ta::TrustAnchor;
|
use crate::data_model::ta::TrustAnchor;
|
||||||
@ -68,12 +69,17 @@ pub enum CcrBuildError {
|
|||||||
#[error("manifest EE certificate SIA is not the EE form: {0}")]
|
#[error("manifest EE certificate SIA is not the EE form: {0}")]
|
||||||
ManifestEeSiaWrongVariant(String),
|
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}")]
|
#[error("manifest child SKI is not valid lowercase hex or not 20 bytes: {0}")]
|
||||||
InvalidChildSki(String),
|
InvalidChildSki(String),
|
||||||
|
|
||||||
#[error("manifest AccessDescription contains unsupported accessMethod OID: {0}")]
|
|
||||||
UnsupportedAccessMethodOid(String),
|
|
||||||
|
|
||||||
#[error("manifest state encoding failed: {0}")]
|
#[error("manifest state encoding failed: {0}")]
|
||||||
ManifestEncode(String),
|
ManifestEncode(String),
|
||||||
|
|
||||||
@ -262,11 +268,16 @@ pub fn build_manifest_state_from_vcirs_with_breakdown(
|
|||||||
CcrBuildError::ManifestEeMissingSia(vcir.current_manifest_rsync_uri.clone())
|
CcrBuildError::ManifestEeMissingSia(vcir.current_manifest_rsync_uri.clone())
|
||||||
})?;
|
})?;
|
||||||
let locations = match sia {
|
let locations = match sia {
|
||||||
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
SubjectInfoAccess::Ee(ee_sia) => vec![
|
||||||
.access_descriptions
|
select_manifest_signed_object_location(
|
||||||
.iter()
|
&vcir.current_manifest_rsync_uri,
|
||||||
.map(encode_access_description_der)
|
&ee_sia.access_descriptions,
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
)
|
||||||
|
.map_err(|detail| CcrBuildError::ManifestSiaLocationSelection {
|
||||||
|
manifest_rsync_uri: vcir.current_manifest_rsync_uri.clone(),
|
||||||
|
detail,
|
||||||
|
})?,
|
||||||
|
],
|
||||||
SubjectInfoAccess::Ca(_) => {
|
SubjectInfoAccess::Ca(_) => {
|
||||||
return Err(CcrBuildError::ManifestEeSiaWrongVariant(
|
return Err(CcrBuildError::ManifestEeSiaWrongVariant(
|
||||||
vcir.current_manifest_rsync_uri.clone(),
|
vcir.current_manifest_rsync_uri.clone(),
|
||||||
@ -353,45 +364,6 @@ fn collect_subordinate_ski_bytes(
|
|||||||
Ok(skis.into_iter().collect())
|
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(
|
pub fn build_router_key_state(
|
||||||
router_certs: &[BgpsecRouterCertificate],
|
router_certs: &[BgpsecRouterCertificate],
|
||||||
) -> Result<RouterKeyState, CcrBuildError> {
|
) -> Result<RouterKeyState, CcrBuildError> {
|
||||||
@ -650,17 +622,33 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sample_manifest_vcir(
|
fn sample_manifest_vcir(manifest_der: &[u8], child_ski_hex: &str) -> ValidatedCaInstanceResult {
|
||||||
manifest_uri: &str,
|
|
||||||
manifest_der: &[u8],
|
|
||||||
child_ski_hex: &str,
|
|
||||||
) -> ValidatedCaInstanceResult {
|
|
||||||
let manifest = ManifestObject::decode_der(manifest_der).expect("decode manifest fixture");
|
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 hash = hex::encode(sha2::Sha256::digest(manifest_der));
|
||||||
let now = manifest.manifest.this_update;
|
let now = manifest.manifest.this_update;
|
||||||
let next = manifest.manifest.next_update;
|
let next = manifest.manifest.next_update;
|
||||||
let projection = crate::storage::VcirCcrManifestProjection {
|
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_sha256: sha2::Sha256::digest(manifest_der).to_vec(),
|
||||||
manifest_size: manifest_der.len() as u64,
|
manifest_size: manifest_der.len() as u64,
|
||||||
manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
|
manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
|
||||||
@ -680,18 +668,19 @@ mod tests {
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("manifest sia")
|
.expect("manifest sia")
|
||||||
{
|
{
|
||||||
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
SubjectInfoAccess::Ee(ee_sia) => vec![
|
||||||
.access_descriptions
|
select_manifest_signed_object_location(
|
||||||
.iter()
|
&manifest_uri,
|
||||||
.map(encode_access_description_der)
|
&ee_sia.access_descriptions,
|
||||||
.collect::<Result<Vec<_>, _>>()
|
)
|
||||||
.expect("encode access descriptions"),
|
.expect("select manifest signedObject"),
|
||||||
|
],
|
||||||
SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
|
SubjectInfoAccess::Ca(_) => panic!("manifest ee sia should not be CA variant"),
|
||||||
},
|
},
|
||||||
subordinate_skis: vec![hex::decode(child_ski_hex).expect("decode child ski")],
|
subordinate_skis: vec![hex::decode(child_ski_hex).expect("decode child ski")],
|
||||||
};
|
};
|
||||||
crate::storage::ValidatedCaInstanceResult {
|
crate::storage::ValidatedCaInstanceResult {
|
||||||
manifest_rsync_uri: manifest_uri.to_string(),
|
manifest_rsync_uri: manifest_uri.clone(),
|
||||||
parent_manifest_rsync_uri: None,
|
parent_manifest_rsync_uri: None,
|
||||||
tal_id: "test-tal".to_string(),
|
tal_id: "test-tal".to_string(),
|
||||||
ca_subject_name: "CN=test".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(
|
last_successful_validation_time: crate::storage::PackTime::from_utc_offset_datetime(
|
||||||
now,
|
now,
|
||||||
),
|
),
|
||||||
current_manifest_rsync_uri: manifest_uri.to_string(),
|
current_manifest_rsync_uri: manifest_uri.clone(),
|
||||||
current_crl_rsync_uri: format!("{manifest_uri}.crl"),
|
current_crl_rsync_uri: format!("{manifest_uri}.crl"),
|
||||||
validated_manifest_meta: crate::storage::ValidatedManifestMeta {
|
validated_manifest_meta: crate::storage::ValidatedManifestMeta {
|
||||||
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
|
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||||
@ -736,7 +725,7 @@ mod tests {
|
|||||||
related_artifacts: vec![crate::storage::VcirRelatedArtifact {
|
related_artifacts: vec![crate::storage::VcirRelatedArtifact {
|
||||||
artifact_role: crate::storage::VcirArtifactRole::Manifest,
|
artifact_role: crate::storage::VcirArtifactRole::Manifest,
|
||||||
artifact_kind: crate::storage::VcirArtifactKind::Mft,
|
artifact_kind: crate::storage::VcirArtifactKind::Mft,
|
||||||
uri: Some(manifest_uri.to_string()),
|
uri: Some(manifest_uri.clone()),
|
||||||
sha256: hash,
|
sha256: hash,
|
||||||
object_type: Some("mft".to_string()),
|
object_type: Some("mft".to_string()),
|
||||||
validation_status: crate::storage::VcirArtifactValidationStatus::Accepted,
|
validation_status: crate::storage::VcirArtifactValidationStatus::Accepted,
|
||||||
@ -766,10 +755,8 @@ mod tests {
|
|||||||
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft",
|
"tests/fixtures/repository/ca.rg.net/rpki/RGnet-OU/bW-_qXU9uNhGQz21NR2ansB8lr0.mft",
|
||||||
))
|
))
|
||||||
.expect("read manifest b");
|
.expect("read manifest b");
|
||||||
let vcir_a =
|
let vcir_a = sample_manifest_vcir(&manifest_a, &"33".repeat(20));
|
||||||
sample_manifest_vcir("rsync://example.test/a.mft", &manifest_a, &"33".repeat(20));
|
let vcir_b = sample_manifest_vcir(&manifest_b, &"44".repeat(20));
|
||||||
let vcir_b =
|
|
||||||
sample_manifest_vcir("rsync://example.test/b.mft", &manifest_b, &"44".repeat(20));
|
|
||||||
let store_dir = tempfile::tempdir().expect("tempdir");
|
let store_dir = tempfile::tempdir().expect("tempdir");
|
||||||
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
let store = RocksStore::open(store_dir.path()).expect("open rocksdb");
|
||||||
for (vcir, bytes) in [(&vcir_a, &manifest_a), (&vcir_b, &manifest_b)] {
|
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 })
|
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 super::*;
|
||||||
use crate::ccr::decode::decode_content_info;
|
use crate::ccr::decode::decode_content_info;
|
||||||
use crate::data_model::manifest::ManifestObject;
|
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::roa::{IpPrefix, RoaAfi};
|
||||||
use crate::data_model::ta::TrustAnchor;
|
use crate::data_model::ta::TrustAnchor;
|
||||||
use crate::data_model::tal::Tal;
|
use crate::data_model::tal::Tal;
|
||||||
@ -149,15 +151,34 @@ mod tests {
|
|||||||
let base = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
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_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 = 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 hash = hex::encode(sha2::Sha256::digest(&manifest_der));
|
||||||
let mut raw = RawByHashEntry::from_bytes(hash.clone(), manifest_der.clone());
|
let mut raw = RawByHashEntry::from_bytes(hash.clone(), manifest_der.clone());
|
||||||
raw.origin_uris
|
raw.origin_uris.push(manifest_uri.clone());
|
||||||
.push("rsync://example.test/repo/current.mft".to_string());
|
|
||||||
raw.object_type = Some("mft".to_string());
|
raw.object_type = Some("mft".to_string());
|
||||||
raw.encoding = Some("der".to_string());
|
raw.encoding = Some("der".to_string());
|
||||||
store.put_raw_by_hash_entry(&raw).expect("put raw");
|
store.put_raw_by_hash_entry(&raw).expect("put raw");
|
||||||
let projection = VcirCcrManifestProjection {
|
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_sha256: sha2::Sha256::digest(&manifest_der).to_vec(),
|
||||||
manifest_size: manifest_der.len() as u64,
|
manifest_size: manifest_der.len() as u64,
|
||||||
manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
|
manifest_ee_aki: manifest.signed_object.signed_data.certificates[0]
|
||||||
@ -169,14 +190,27 @@ mod tests {
|
|||||||
.expect("manifest aki"),
|
.expect("manifest aki"),
|
||||||
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
manifest_number_be: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||||
manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
|
manifest_this_update: PackTime::from_utc_offset_datetime(manifest.manifest.this_update),
|
||||||
manifest_sia_locations_der: vec![vec![
|
manifest_sia_locations_der: match manifest.signed_object.signed_data.certificates[0]
|
||||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
.resource_cert
|
||||||
b'r', b's', b'y', b'n', b'c',
|
.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]],
|
subordinate_skis: vec![vec![0x33; 20]],
|
||||||
};
|
};
|
||||||
ValidatedCaInstanceResult {
|
ValidatedCaInstanceResult {
|
||||||
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
manifest_rsync_uri: manifest_uri.clone(),
|
||||||
parent_manifest_rsync_uri: None,
|
parent_manifest_rsync_uri: None,
|
||||||
tal_id: "apnic".to_string(),
|
tal_id: "apnic".to_string(),
|
||||||
ca_subject_name: "CN=test".to_string(),
|
ca_subject_name: "CN=test".to_string(),
|
||||||
@ -185,7 +219,7 @@ mod tests {
|
|||||||
last_successful_validation_time: PackTime::from_utc_offset_datetime(
|
last_successful_validation_time: PackTime::from_utc_offset_datetime(
|
||||||
manifest.manifest.this_update,
|
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(),
|
current_crl_rsync_uri: "rsync://example.test/repo/current.crl".to_string(),
|
||||||
validated_manifest_meta: ValidatedManifestMeta {
|
validated_manifest_meta: ValidatedManifestMeta {
|
||||||
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
|
validated_manifest_number: manifest.manifest.manifest_number.bytes_be.clone(),
|
||||||
@ -229,7 +263,7 @@ mod tests {
|
|||||||
related_artifacts: vec![VcirRelatedArtifact {
|
related_artifacts: vec![VcirRelatedArtifact {
|
||||||
artifact_role: VcirArtifactRole::Manifest,
|
artifact_role: VcirArtifactRole::Manifest,
|
||||||
artifact_kind: VcirArtifactKind::Mft,
|
artifact_kind: VcirArtifactKind::Mft,
|
||||||
uri: Some("rsync://example.test/repo/current.mft".to_string()),
|
uri: Some(manifest_uri),
|
||||||
sha256: hash,
|
sha256: hash,
|
||||||
object_type: Some("mft".to_string()),
|
object_type: Some("mft".to_string()),
|
||||||
validation_status: VcirArtifactValidationStatus::Accepted,
|
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")]
|
#[cfg(feature = "full")]
|
||||||
pub mod export;
|
pub mod export;
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub(crate) mod manifest_location;
|
||||||
pub mod model;
|
pub mod model;
|
||||||
pub mod state_digest;
|
pub mod state_digest;
|
||||||
#[cfg(feature = "full")]
|
#[cfg(feature = "full")]
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::data_model::oid::OID_AD_SIGNED_OBJECT;
|
||||||
|
use crate::data_model::rc::AccessDescription;
|
||||||
use crate::memory_telemetry::{
|
use crate::memory_telemetry::{
|
||||||
MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot,
|
MemoryTelemetryCheckpoint, MemoryTelemetrySummary, ProcessMemorySnapshot,
|
||||||
};
|
};
|
||||||
@ -1722,8 +1724,9 @@ fn sample_cli_ccr_accumulator() -> CcrAccumulator {
|
|||||||
)
|
)
|
||||||
.expect("discover root");
|
.expect("discover root");
|
||||||
let mut accumulator = CcrAccumulator::new(vec![discovery.trust_anchor.clone()]);
|
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 {
|
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_sha256: vec![0x44; 32],
|
||||||
manifest_size: 2048,
|
manifest_size: 2048,
|
||||||
manifest_ee_aki: vec![0x55; 20],
|
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(
|
manifest_this_update: crate::storage::PackTime::from_utc_offset_datetime(
|
||||||
time::OffsetDateTime::now_utc(),
|
time::OffsetDateTime::now_utc(),
|
||||||
),
|
),
|
||||||
manifest_sia_locations_der: vec![vec![
|
manifest_sia_locations_der: vec![
|
||||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
crate::ccr::manifest_location::encode_access_description_der(&AccessDescription {
|
||||||
b'r', b's', b'y', b'n', b'c',
|
access_method_oid: OID_AD_SIGNED_OBJECT.to_string(),
|
||||||
]],
|
access_location: manifest_uri,
|
||||||
|
})
|
||||||
|
.expect("encode signedObject"),
|
||||||
|
],
|
||||||
subordinate_skis: vec![vec![0x33; 20]],
|
subordinate_skis: vec![vec![0x33; 20]],
|
||||||
};
|
};
|
||||||
accumulator
|
accumulator
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
mod vcir_der;
|
|
||||||
|
|
||||||
use crate::analysis::timing::TimingHandle;
|
use crate::analysis::timing::TimingHandle;
|
||||||
use crate::audit::{
|
use crate::audit::{
|
||||||
AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit,
|
AuditObjectKind, AuditObjectResult, AuditWarning, ObjectAuditEntry, PublicationPointAudit,
|
||||||
@ -69,7 +67,7 @@ use base64::Engine as _;
|
|||||||
use x509_parser::prelude::FromDer;
|
use x509_parser::prelude::FromDer;
|
||||||
use x509_parser::x509::SubjectPublicKeyInfo;
|
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_PARALLEL_MIN_CHILDREN: usize = 256;
|
||||||
const PUBLICATION_POINT_CACHE_CHILD_RESTORE_MAX_WORKERS: usize = 16;
|
const PUBLICATION_POINT_CACHE_CHILD_RESTORE_MAX_WORKERS: usize = 16;
|
||||||
@ -5677,11 +5675,10 @@ fn build_vcir_ccr_manifest_projection_from_fresh(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| "manifest EE certificate missing Subject Information Access".to_string())?
|
.ok_or_else(|| "manifest EE certificate missing Subject Information Access".to_string())?
|
||||||
{
|
{
|
||||||
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
SubjectInfoAccess::Ee(ee_sia) => vec![select_manifest_signed_object_location(
|
||||||
.access_descriptions
|
&ca.manifest_rsync_uri,
|
||||||
.iter()
|
&ee_sia.access_descriptions,
|
||||||
.map(encode_access_description_der_for_vcir_ccr_projection)
|
)?],
|
||||||
.collect::<Result<Vec<_>, _>>()?,
|
|
||||||
SubjectInfoAccess::Ca(_) => {
|
SubjectInfoAccess::Ca(_) => {
|
||||||
return Err(
|
return Err(
|
||||||
"manifest EE certificate Subject Information Access has CA variant".to_string(),
|
"manifest EE certificate Subject Information Access has CA variant".to_string(),
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
use super::*;
|
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::data_model::roa::RoaAfi;
|
||||||
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
use crate::fetch::rsync::LocalDirRsyncFetcher;
|
||||||
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
|
use crate::fetch::rsync::{RsyncFetchError, RsyncFetcher};
|
||||||
@ -603,10 +606,13 @@ fn sample_vcir_for_projection(
|
|||||||
manifest_ee_aki: vec![0x11; 20],
|
manifest_ee_aki: vec![0x11; 20],
|
||||||
manifest_number_be: vec![1],
|
manifest_number_be: vec![1],
|
||||||
manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
manifest_this_update: PackTime::from_utc_offset_datetime(now),
|
||||||
manifest_sia_locations_der: vec![vec![
|
manifest_sia_locations_der: vec![
|
||||||
0x30, 0x11, 0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x05, 0x86, 0x05,
|
crate::ccr::manifest_location::encode_access_description_der(&AccessDescription {
|
||||||
b'r', b's', b'y', b'n', b'c',
|
access_method_oid: OID_AD_SIGNED_OBJECT.to_string(),
|
||||||
]],
|
access_location: manifest_uri.clone(),
|
||||||
|
})
|
||||||
|
.expect("encode signedObject"),
|
||||||
|
],
|
||||||
subordinate_skis: vec![vec![0x33; 20]],
|
subordinate_skis: vec![vec![0x33; 20]],
|
||||||
};
|
};
|
||||||
ValidatedCaInstanceResult {
|
ValidatedCaInstanceResult {
|
||||||
@ -1252,12 +1258,13 @@ fn build_vcir_ccr_manifest_projection_from_fresh_real_snapshot_matches_manifest_
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("manifest sia")
|
.expect("manifest sia")
|
||||||
{
|
{
|
||||||
SubjectInfoAccess::Ee(ee_sia) => ee_sia
|
SubjectInfoAccess::Ee(ee_sia) => vec![
|
||||||
.access_descriptions
|
crate::ccr::manifest_location::select_manifest_signed_object_location(
|
||||||
.iter()
|
&pack.manifest_rsync_uri,
|
||||||
.map(encode_access_description_der_for_vcir_ccr_projection)
|
&ee_sia.access_descriptions,
|
||||||
.collect::<Result<Vec<_>, _>>()
|
)
|
||||||
.expect("encode locations"),
|
.expect("select manifest signedObject"),
|
||||||
|
],
|
||||||
SubjectInfoAccess::Ca(_) => panic!("manifest ee SIA should not be CA variant"),
|
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