669 lines
25 KiB
Rust
669 lines
25 KiB
Rust
use std::net::{Ipv4Addr, Ipv6Addr};
|
|
use std::path::Path;
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::{Value, json};
|
|
use sha2::{Digest, Sha256};
|
|
|
|
use crate::data_model::aspa::AspaObject;
|
|
use crate::data_model::crl::RpkixCrl;
|
|
use crate::data_model::manifest::ManifestObject;
|
|
use crate::data_model::rc::{
|
|
AccessDescription, RcExtensions, ResourceCertificate, SubjectInfoAccess,
|
|
};
|
|
use crate::data_model::roa::{IpPrefix as RoaIpPrefix, RoaAfi, RoaObject};
|
|
use crate::data_model::signed_object::{
|
|
ResourceEeCertificate, RpkiSignedObject, SignedAttrsProfiled, SignerInfoProfiled,
|
|
};
|
|
use crate::data_model::ta::TaCertificate;
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ObjectType {
|
|
Auto,
|
|
Cer,
|
|
Mft,
|
|
Crl,
|
|
Roa,
|
|
Aspa,
|
|
}
|
|
|
|
impl ObjectType {
|
|
pub fn parse(value: &str) -> Result<Self, String> {
|
|
match value.to_ascii_lowercase().as_str() {
|
|
"auto" => Ok(Self::Auto),
|
|
"cer" | ".cer" | "cert" | "certificate" => Ok(Self::Cer),
|
|
"mft" | ".mft" | "manifest" => Ok(Self::Mft),
|
|
"crl" | ".crl" => Ok(Self::Crl),
|
|
"roa" | ".roa" => Ok(Self::Roa),
|
|
"asa" | ".asa" | "aspa" => Ok(Self::Aspa),
|
|
_ => Err(format!("unsupported object type: {value}")),
|
|
}
|
|
}
|
|
|
|
pub fn label(self) -> &'static str {
|
|
object_type_label(self)
|
|
}
|
|
}
|
|
|
|
pub fn resolve_object_type(object_type: ObjectType, path: &Path) -> Result<ObjectType, String> {
|
|
if object_type != ObjectType::Auto {
|
|
return Ok(object_type);
|
|
}
|
|
match path
|
|
.extension()
|
|
.and_then(|v| v.to_str())
|
|
.map(|v| v.to_ascii_lowercase())
|
|
.as_deref()
|
|
{
|
|
Some("cer") => Ok(ObjectType::Cer),
|
|
Some("mft") => Ok(ObjectType::Mft),
|
|
Some("crl") => Ok(ObjectType::Crl),
|
|
Some("roa") => Ok(ObjectType::Roa),
|
|
Some("asa") | Some("aspa") => Ok(ObjectType::Aspa),
|
|
_ => Err(format!(
|
|
"cannot infer object type from path: {}",
|
|
path.display()
|
|
)),
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct ObjectProjectionRecord {
|
|
pub schema_version: u32,
|
|
pub sha256: String,
|
|
pub object_type: String,
|
|
pub parse_status: String,
|
|
pub error_summary: Option<String>,
|
|
pub projection: Value,
|
|
}
|
|
|
|
pub fn build_object_projection(
|
|
object_type: ObjectType,
|
|
input_path: &Path,
|
|
bytes: &[u8],
|
|
entry_limit: usize,
|
|
) -> ObjectProjectionRecord {
|
|
let resolved = match resolve_object_type(object_type, input_path) {
|
|
Ok(value) => value,
|
|
Err(err) => {
|
|
return ObjectProjectionRecord {
|
|
schema_version: 1,
|
|
sha256: sha256_hex(bytes),
|
|
object_type: "unknown".to_string(),
|
|
parse_status: "error".to_string(),
|
|
error_summary: Some(err),
|
|
projection: json!({"decode": {"profileValid": false}}),
|
|
};
|
|
}
|
|
};
|
|
let projection = parse_object_json(resolved, input_path, bytes, entry_limit);
|
|
let parse_status = if projection
|
|
.get("object")
|
|
.and_then(|v| v.get("decode"))
|
|
.and_then(|v| v.get("profileValid"))
|
|
.and_then(Value::as_bool)
|
|
.unwrap_or(false)
|
|
{
|
|
"ok"
|
|
} else {
|
|
"error"
|
|
};
|
|
let error_summary = projection
|
|
.get("object")
|
|
.and_then(|v| v.get("decode"))
|
|
.and_then(|v| v.get("error"))
|
|
.and_then(Value::as_str)
|
|
.map(str::to_string);
|
|
ObjectProjectionRecord {
|
|
schema_version: 1,
|
|
sha256: sha256_hex(bytes),
|
|
object_type: resolved.label().to_string(),
|
|
parse_status: parse_status.to_string(),
|
|
error_summary,
|
|
projection,
|
|
}
|
|
}
|
|
|
|
pub fn parse_object_json(
|
|
object_type: ObjectType,
|
|
input_path: &Path,
|
|
bytes: &[u8],
|
|
entry_limit: usize,
|
|
) -> Value {
|
|
let object = match object_type {
|
|
ObjectType::Auto => unreachable!("auto must be resolved"),
|
|
ObjectType::Cer => parse_cer_json(bytes),
|
|
ObjectType::Mft => parse_mft_json(bytes, entry_limit),
|
|
ObjectType::Crl => parse_crl_json(bytes, entry_limit),
|
|
ObjectType::Roa => parse_roa_json(bytes, entry_limit),
|
|
ObjectType::Aspa => parse_aspa_json(bytes, entry_limit),
|
|
};
|
|
json!({
|
|
"tool": "rpki_object_parse",
|
|
"schemaVersion": 1,
|
|
"input": {
|
|
"path": input_path.display().to_string(),
|
|
"type": object_type_label(object_type),
|
|
"bytes": bytes_summary(bytes),
|
|
},
|
|
"object": object,
|
|
})
|
|
}
|
|
|
|
pub fn parse_cer_json(bytes: &[u8]) -> Value {
|
|
match ResourceCertificate::decode_der(bytes) {
|
|
Ok(cert) => {
|
|
let ta_profile = match TaCertificate::decode_der(bytes) {
|
|
Ok(ta) => json!({
|
|
"valid": true,
|
|
"selfSignature": result_json(ta.verify_self_signature().map_err(|e| e.to_string())),
|
|
}),
|
|
Err(err) => json!({
|
|
"valid": false,
|
|
"error": err.to_string(),
|
|
}),
|
|
};
|
|
json!({
|
|
"type": "cer",
|
|
"decode": {"profileValid": true},
|
|
"resourceCertificate": resource_certificate_json(&cert),
|
|
"trustAnchorProfile": ta_profile,
|
|
})
|
|
}
|
|
Err(err) => json!({
|
|
"type": "cer",
|
|
"decode": {"profileValid": false, "error": err.to_string()},
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn parse_mft_json(bytes: &[u8], entry_limit: usize) -> Value {
|
|
match ManifestObject::decode_der(bytes) {
|
|
Ok(mft) => {
|
|
let files = mft.manifest.parse_files();
|
|
let (file_sample, file_list_error) = match files {
|
|
Ok(entries) => (
|
|
json!({
|
|
"count": entries.len(),
|
|
"truncated": entries.len() > entry_limit,
|
|
"entries": entries.iter().take(entry_limit).map(|item| {
|
|
json!({"fileName": item.file_name, "hashHex": hex::encode(item.hash_bytes)})
|
|
}).collect::<Vec<_>>(),
|
|
}),
|
|
Value::Null,
|
|
),
|
|
Err(err) => (Value::Null, json!(err.to_string())),
|
|
};
|
|
json!({
|
|
"type": "mft",
|
|
"decode": {"profileValid": true},
|
|
"eContentType": mft.econtent_type,
|
|
"signedObject": signed_object_json(&mft.signed_object),
|
|
"manifest": {
|
|
"version": mft.manifest.version,
|
|
"manifestNumberHex": mft.manifest.manifest_number.to_hex_upper(),
|
|
"thisUpdate": format_time(mft.manifest.this_update),
|
|
"nextUpdate": format_time(mft.manifest.next_update),
|
|
"fileHashAlg": mft.manifest.file_hash_alg,
|
|
"fileCount": mft.manifest.file_count(),
|
|
"fileList": file_sample,
|
|
"fileListError": file_list_error,
|
|
},
|
|
"embeddedEeProfile": result_json(mft.validate_embedded_ee_cert().map_err(|e| e.to_string())),
|
|
"cmsSignature": result_json(mft.signed_object.verify_signature().map_err(|e| e.to_string())),
|
|
})
|
|
}
|
|
Err(err) => json!({
|
|
"type": "mft",
|
|
"decode": {"profileValid": false, "error": err.to_string()},
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn parse_crl_json(bytes: &[u8], entry_limit: usize) -> Value {
|
|
match RpkixCrl::decode_der(bytes) {
|
|
Ok(crl) => json!({
|
|
"type": "crl",
|
|
"decode": {"profileValid": true},
|
|
"rawDer": bytes_summary(&crl.raw_der),
|
|
"version": crl.version,
|
|
"issuer": crl.issuer_dn,
|
|
"signatureAlgorithm": crl.signature_algorithm_oid,
|
|
"thisUpdate": format_time(crl.this_update.utc),
|
|
"nextUpdate": format_time(crl.next_update.utc),
|
|
"extensions": {
|
|
"authorityKeyIdentifier": hex::encode(&crl.extensions.authority_key_identifier),
|
|
"crlNumberHex": crl.extensions.crl_number.to_hex_upper(),
|
|
"crlNumber": crl.extensions.crl_number.to_u64(),
|
|
},
|
|
"revokedCertificates": {
|
|
"count": crl.revoked_certs.len(),
|
|
"truncated": crl.revoked_certs.len() > entry_limit,
|
|
"entries": crl.revoked_certs.iter().take(entry_limit).map(|item| {
|
|
json!({
|
|
"serialNumberHex": item.serial_number.to_hex_upper(),
|
|
"serialNumber": item.serial_number.to_u64(),
|
|
"revocationDate": format_time(item.revocation_date.utc),
|
|
})
|
|
}).collect::<Vec<_>>(),
|
|
},
|
|
}),
|
|
Err(err) => json!({
|
|
"type": "crl",
|
|
"decode": {"profileValid": false, "error": err.to_string()},
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn manifest_file_entries_page(
|
|
bytes: &[u8],
|
|
offset: usize,
|
|
limit: usize,
|
|
) -> Result<(usize, Vec<Value>), String> {
|
|
let mft = ManifestObject::decode_der(bytes).map_err(|err| err.to_string())?;
|
|
let entries = mft.manifest.parse_files().map_err(|err| err.to_string())?;
|
|
let total = entries.len();
|
|
let end = (offset + limit).min(total);
|
|
let page = entries[offset.min(total)..end]
|
|
.iter()
|
|
.map(|item| json!({"fileName": item.file_name, "hashHex": hex::encode(&item.hash_bytes)}))
|
|
.collect::<Vec<_>>();
|
|
Ok((total, page))
|
|
}
|
|
|
|
pub fn crl_revoked_entries_page(
|
|
bytes: &[u8],
|
|
offset: usize,
|
|
limit: usize,
|
|
) -> Result<(usize, Vec<Value>), String> {
|
|
let crl = RpkixCrl::decode_der(bytes).map_err(|err| err.to_string())?;
|
|
let total = crl.revoked_certs.len();
|
|
let end = (offset + limit).min(total);
|
|
let page = crl.revoked_certs[offset.min(total)..end]
|
|
.iter()
|
|
.map(|item| {
|
|
json!({
|
|
"serialNumberHex": item.serial_number.to_hex_upper(),
|
|
"serialNumber": item.serial_number.to_u64(),
|
|
"revocationDate": format_time(item.revocation_date.utc),
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
Ok((total, page))
|
|
}
|
|
|
|
pub fn parse_roa_json(bytes: &[u8], entry_limit: usize) -> Value {
|
|
match RoaObject::decode_der(bytes) {
|
|
Ok(roa) => json!({
|
|
"type": "roa",
|
|
"decode": {"profileValid": true},
|
|
"eContentType": roa.econtent_type,
|
|
"signedObject": signed_object_json(&roa.signed_object),
|
|
"roa": {
|
|
"version": roa.roa.version,
|
|
"asId": roa.roa.as_id,
|
|
"ipAddressFamilies": roa.roa.ip_addr_blocks.iter().map(|family| {
|
|
json!({
|
|
"afi": format!("{:?}", family.afi),
|
|
"addressCount": family.addresses.len(),
|
|
"truncated": family.addresses.len() > entry_limit,
|
|
"addresses": family.addresses.iter().take(entry_limit).map(|entry| {
|
|
json!({
|
|
"prefix": roa_prefix_string(&entry.prefix),
|
|
"maxLength": entry.max_length,
|
|
})
|
|
}).collect::<Vec<_>>(),
|
|
})
|
|
}).collect::<Vec<_>>(),
|
|
},
|
|
"embeddedEeProfile": result_json(roa.validate_embedded_ee_cert().map_err(|e| e.to_string())),
|
|
"cmsSignature": result_json(roa.signed_object.verify_signature().map_err(|e| e.to_string())),
|
|
}),
|
|
Err(err) => json!({
|
|
"type": "roa",
|
|
"decode": {"profileValid": false, "error": err.to_string()},
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn parse_aspa_json(bytes: &[u8], entry_limit: usize) -> Value {
|
|
match AspaObject::decode_der(bytes) {
|
|
Ok(aspa) => json!({
|
|
"type": "aspa",
|
|
"decode": {"profileValid": true},
|
|
"eContentType": aspa.econtent_type,
|
|
"signedObject": signed_object_json(&aspa.signed_object),
|
|
"aspa": {
|
|
"version": aspa.aspa.version,
|
|
"customerAsId": aspa.aspa.customer_as_id,
|
|
"providerCount": aspa.aspa.provider_as_ids.len(),
|
|
"providersTruncated": aspa.aspa.provider_as_ids.len() > entry_limit,
|
|
"providerAsIds": aspa.aspa.provider_as_ids.iter().take(entry_limit).copied().collect::<Vec<_>>(),
|
|
},
|
|
"embeddedEeProfile": result_json(aspa.validate_embedded_ee_cert().map_err(|e| e.to_string())),
|
|
"cmsSignature": result_json(aspa.signed_object.verify_signature().map_err(|e| e.to_string())),
|
|
}),
|
|
Err(err) => json!({
|
|
"type": "aspa",
|
|
"decode": {"profileValid": false, "error": err.to_string()},
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn resource_certificate_json(cert: &ResourceCertificate) -> Value {
|
|
let tbs = &cert.tbs;
|
|
json!({
|
|
"rawDer": bytes_summary(&cert.raw_der),
|
|
"kind": format!("{:?}", cert.kind),
|
|
"version": tbs.version,
|
|
"serialNumberHex": hex::encode(tbs.serial_number.to_bytes_be()),
|
|
"signatureAlgorithm": tbs.signature_algorithm,
|
|
"issuer": tbs.issuer_name.to_string(),
|
|
"subject": tbs.subject_name.to_string(),
|
|
"validity": {
|
|
"notBefore": format_time(tbs.validity_not_before),
|
|
"notAfter": format_time(tbs.validity_not_after),
|
|
},
|
|
"subjectPublicKeyInfo": bytes_summary(&tbs.subject_public_key_info),
|
|
"extensions": rc_extensions_json(&tbs.extensions),
|
|
})
|
|
}
|
|
|
|
fn rc_extensions_json(ext: &RcExtensions) -> Value {
|
|
json!({
|
|
"basicConstraintsCa": ext.basic_constraints_ca,
|
|
"subjectKeyIdentifier": ext.subject_key_identifier.as_ref().map(|v| hex::encode(v)),
|
|
"authorityKeyIdentifier": ext.authority_key_identifier.as_ref().map(|v| hex::encode(v)),
|
|
"crlDistributionPointsUris": ext.crl_distribution_points_uris,
|
|
"caIssuersUris": ext.ca_issuers_uris,
|
|
"subjectInfoAccess": subject_info_access_json(ext.subject_info_access.as_ref()),
|
|
"certificatePoliciesOid": ext.certificate_policies_oid,
|
|
"ipResources": serde_json::to_value(&ext.ip_resources).unwrap_or(Value::Null),
|
|
"asResources": serde_json::to_value(&ext.as_resources).unwrap_or(Value::Null),
|
|
})
|
|
}
|
|
|
|
fn subject_info_access_json(value: Option<&SubjectInfoAccess>) -> Value {
|
|
match value {
|
|
None => Value::Null,
|
|
Some(SubjectInfoAccess::Ca(ca)) => json!({
|
|
"kind": "ca",
|
|
"accessDescriptions": ca.access_descriptions.iter().map(access_description_json).collect::<Vec<_>>(),
|
|
}),
|
|
Some(SubjectInfoAccess::Ee(ee)) => json!({
|
|
"kind": "ee",
|
|
"signedObjectUris": ee.signed_object_uris,
|
|
"accessDescriptions": ee.access_descriptions.iter().map(access_description_json).collect::<Vec<_>>(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn access_description_json(value: &AccessDescription) -> Value {
|
|
json!({
|
|
"accessMethodOid": value.access_method_oid,
|
|
"accessLocation": value.access_location,
|
|
})
|
|
}
|
|
|
|
fn signed_object_json(signed_object: &RpkiSignedObject) -> Value {
|
|
let signed_data = &signed_object.signed_data;
|
|
json!({
|
|
"rawDer": bytes_summary(&signed_object.raw_der),
|
|
"contentInfoContentType": signed_object.content_info_content_type,
|
|
"signedData": {
|
|
"version": signed_data.version,
|
|
"digestAlgorithms": signed_data.digest_algorithms,
|
|
"encapContentInfo": {
|
|
"eContentType": signed_data.encap_content_info.econtent_type,
|
|
"eContent": bytes_summary(&signed_data.encap_content_info.econtent),
|
|
},
|
|
"certificates": signed_data.certificates.iter().map(ee_certificate_json).collect::<Vec<_>>(),
|
|
"crlsPresent": signed_data.crls_present,
|
|
"signerInfos": signed_data.signer_infos.iter().map(signer_info_json).collect::<Vec<_>>(),
|
|
},
|
|
})
|
|
}
|
|
|
|
fn ee_certificate_json(cert: &ResourceEeCertificate) -> Value {
|
|
json!({
|
|
"rawDer": bytes_summary(&cert.raw_der),
|
|
"subjectKeyIdentifier": hex::encode(&cert.subject_key_identifier),
|
|
"spkiDer": bytes_summary(&cert.spki_der),
|
|
"rsaPublicKey": {
|
|
"modulus": bytes_summary(&cert.rsa_public_modulus),
|
|
"exponent": bytes_summary(&cert.rsa_public_exponent),
|
|
},
|
|
"tbsCertificate": bytes_summary(&cert.tbs_certificate_der),
|
|
"certificateSignature": bytes_summary(&cert.signature_bytes),
|
|
"keyUsageSummary": format!("{:?}", cert.key_usage_summary),
|
|
"siaSignedObjectUris": cert.sia_signed_object_uris,
|
|
"resourceCertificate": resource_certificate_json(&cert.resource_cert),
|
|
})
|
|
}
|
|
|
|
fn signer_info_json(info: &SignerInfoProfiled) -> Value {
|
|
json!({
|
|
"version": info.version,
|
|
"sidSki": hex::encode(&info.sid_ski),
|
|
"digestAlgorithm": info.digest_algorithm,
|
|
"signatureAlgorithm": info.signature_algorithm,
|
|
"signedAttrs": signed_attrs_json(&info.signed_attrs),
|
|
"unsignedAttrsPresent": info.unsigned_attrs_present,
|
|
"signature": bytes_summary(&info.signature),
|
|
"signedAttrsDerForSignature": bytes_summary(&info.signed_attrs_der_for_signature),
|
|
})
|
|
}
|
|
|
|
fn signed_attrs_json(attrs: &SignedAttrsProfiled) -> Value {
|
|
json!({
|
|
"contentType": attrs.content_type,
|
|
"messageDigest": hex::encode(&attrs.message_digest),
|
|
"signingTime": {
|
|
"utc": format_time(attrs.signing_time.utc),
|
|
"encoding": format!("{:?}", attrs.signing_time.encoding),
|
|
},
|
|
"otherAttrsPresent": attrs.other_attrs_present,
|
|
})
|
|
}
|
|
|
|
fn result_json(result: Result<(), String>) -> Value {
|
|
match result {
|
|
Ok(()) => json!({"valid": true}),
|
|
Err(err) => json!({"valid": false, "error": err}),
|
|
}
|
|
}
|
|
|
|
fn object_type_label(object_type: ObjectType) -> &'static str {
|
|
match object_type {
|
|
ObjectType::Auto => "auto",
|
|
ObjectType::Cer => "cer",
|
|
ObjectType::Mft => "mft",
|
|
ObjectType::Crl => "crl",
|
|
ObjectType::Roa => "roa",
|
|
ObjectType::Aspa => "aspa",
|
|
}
|
|
}
|
|
|
|
fn bytes_summary(bytes: &[u8]) -> Value {
|
|
let head_len = bytes.len().min(16);
|
|
let tail_len = bytes.len().min(16);
|
|
json!({
|
|
"len": bytes.len(),
|
|
"sha256": sha256_hex(bytes),
|
|
"headHex": hex::encode(&bytes[..head_len]),
|
|
"tailHex": hex::encode(&bytes[bytes.len().saturating_sub(tail_len)..]),
|
|
})
|
|
}
|
|
|
|
fn sha256_hex(bytes: &[u8]) -> String {
|
|
hex::encode(Sha256::digest(bytes))
|
|
}
|
|
|
|
fn format_time(value: time::OffsetDateTime) -> String {
|
|
value
|
|
.to_offset(time::UtcOffset::UTC)
|
|
.format(&time::format_description::well_known::Rfc3339)
|
|
.unwrap_or_else(|_| value.unix_timestamp().to_string())
|
|
}
|
|
|
|
fn roa_prefix_string(prefix: &RoaIpPrefix) -> String {
|
|
let bytes = prefix.addr_bytes();
|
|
match prefix.afi {
|
|
RoaAfi::Ipv4 => {
|
|
let octets = [bytes[0], bytes[1], bytes[2], bytes[3]];
|
|
format!("{}/{}", Ipv4Addr::from(octets), prefix.prefix_len)
|
|
}
|
|
RoaAfi::Ipv6 => {
|
|
let mut octets = [0u8; 16];
|
|
octets.copy_from_slice(bytes);
|
|
format!("{}/{}", Ipv6Addr::from(octets), prefix.prefix_len)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::path::Path;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn object_type_parser_and_resolver_cover_aliases() {
|
|
assert_eq!(ObjectType::parse("auto").unwrap(), ObjectType::Auto);
|
|
assert_eq!(ObjectType::parse(".cer").unwrap(), ObjectType::Cer);
|
|
assert_eq!(ObjectType::parse("certificate").unwrap(), ObjectType::Cer);
|
|
assert_eq!(ObjectType::parse("manifest").unwrap(), ObjectType::Mft);
|
|
assert_eq!(ObjectType::parse(".crl").unwrap(), ObjectType::Crl);
|
|
assert_eq!(ObjectType::parse("roa").unwrap(), ObjectType::Roa);
|
|
assert_eq!(ObjectType::parse("aspa").unwrap(), ObjectType::Aspa);
|
|
assert_eq!(ObjectType::parse(".asa").unwrap(), ObjectType::Aspa);
|
|
assert!(ObjectType::parse("unknown").is_err());
|
|
assert_eq!(ObjectType::Aspa.label(), "aspa");
|
|
|
|
assert_eq!(
|
|
resolve_object_type(ObjectType::Auto, Path::new("repo/a.cer")).unwrap(),
|
|
ObjectType::Cer
|
|
);
|
|
assert_eq!(
|
|
resolve_object_type(ObjectType::Auto, Path::new("repo/a.mft")).unwrap(),
|
|
ObjectType::Mft
|
|
);
|
|
assert_eq!(
|
|
resolve_object_type(ObjectType::Auto, Path::new("repo/a.crl")).unwrap(),
|
|
ObjectType::Crl
|
|
);
|
|
assert_eq!(
|
|
resolve_object_type(ObjectType::Auto, Path::new("repo/a.roa")).unwrap(),
|
|
ObjectType::Roa
|
|
);
|
|
assert_eq!(
|
|
resolve_object_type(ObjectType::Auto, Path::new("repo/a.asa")).unwrap(),
|
|
ObjectType::Aspa
|
|
);
|
|
assert_eq!(
|
|
resolve_object_type(ObjectType::Roa, Path::new("repo/a.bin")).unwrap(),
|
|
ObjectType::Roa
|
|
);
|
|
assert!(resolve_object_type(ObjectType::Auto, Path::new("repo/a.bin")).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn invalid_der_returns_error_projection_for_all_object_types() {
|
|
let bytes = b"not der";
|
|
for object_type in [
|
|
ObjectType::Cer,
|
|
ObjectType::Mft,
|
|
ObjectType::Crl,
|
|
ObjectType::Roa,
|
|
ObjectType::Aspa,
|
|
] {
|
|
let value = parse_object_json(object_type, Path::new("bad.der"), bytes, 1);
|
|
assert_eq!(
|
|
value["object"]["decode"]["profileValid"].as_bool(),
|
|
Some(false)
|
|
);
|
|
assert!(value["object"]["decode"]["error"].as_str().is_some());
|
|
}
|
|
|
|
let record = build_object_projection(ObjectType::Auto, Path::new("bad.bin"), bytes, 1);
|
|
assert_eq!(record.object_type, "unknown");
|
|
assert_eq!(record.parse_status, "error");
|
|
assert!(record.error_summary.is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn parses_fixture_objects_into_human_readable_projection() {
|
|
let cases = [
|
|
(
|
|
ObjectType::Cer,
|
|
"tests/fixtures/ta/apnic-ta.cer",
|
|
"cer",
|
|
"resourceCertificate",
|
|
),
|
|
(
|
|
ObjectType::Mft,
|
|
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
|
"mft",
|
|
"manifest",
|
|
),
|
|
(
|
|
ObjectType::Crl,
|
|
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
|
"crl",
|
|
"revokedCertificates",
|
|
),
|
|
(
|
|
ObjectType::Roa,
|
|
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/AS4538.roa",
|
|
"roa",
|
|
"roa",
|
|
),
|
|
(
|
|
ObjectType::Aspa,
|
|
"tests/fixtures/repository/chloe.sobornost.net/rpki/RIPE-nljobsnijders/5m80fwYws_3FiFD7JiQjAqZ1RYQ.asa",
|
|
"aspa",
|
|
"aspa",
|
|
),
|
|
];
|
|
|
|
for (object_type, path, expected_type, expected_section) in cases {
|
|
let bytes = std::fs::read(path).expect("fixture");
|
|
let record = build_object_projection(object_type, Path::new(path), &bytes, 1);
|
|
assert_eq!(record.object_type, expected_type);
|
|
assert_eq!(record.parse_status, "ok");
|
|
assert_eq!(
|
|
record.projection["object"]["decode"]["profileValid"].as_bool(),
|
|
Some(true)
|
|
);
|
|
assert!(record.projection["object"][expected_section].is_object());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn large_projection_lists_are_paged_from_raw_bytes() {
|
|
let mft_bytes = std::fs::read(
|
|
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.mft",
|
|
)
|
|
.expect("mft");
|
|
let (total, page) = manifest_file_entries_page(&mft_bytes, 1, 3).expect("mft page");
|
|
assert!(total >= 3);
|
|
assert_eq!(page.len(), 3);
|
|
assert!(page[0]["fileName"].as_str().is_some());
|
|
let (_, empty_page) =
|
|
manifest_file_entries_page(&mft_bytes, total + 10, 3).expect("empty page");
|
|
assert!(empty_page.is_empty());
|
|
|
|
let crl_bytes = std::fs::read(
|
|
"tests/fixtures/repository/rpki.cernet.net/repo/cernet/0/05FC9C5B88506F7C0D3F862C8895BED67E9F8EBA.crl",
|
|
)
|
|
.expect("crl");
|
|
let (total, page) = crl_revoked_entries_page(&crl_bytes, 0, 5).expect("crl page");
|
|
assert!(page.len() <= total);
|
|
let (_, empty_page) =
|
|
crl_revoked_entries_page(&crl_bytes, total + 10, 5).expect("empty crl page");
|
|
assert!(empty_page.is_empty());
|
|
}
|
|
}
|