2166 lines
75 KiB
Rust
2166 lines
75 KiB
Rust
use std::collections::HashSet;
|
|
use std::path::Path;
|
|
|
|
use rocksdb::{
|
|
ColumnFamily, ColumnFamilyDescriptor, DB, DBCompressionType, Direction, IteratorMode, Options,
|
|
WriteBatch,
|
|
};
|
|
use serde::{Deserialize, Serialize, de::DeserializeOwned};
|
|
use sha2::Digest;
|
|
|
|
use crate::data_model::rc::{AsResourceSet, IpResourceSet};
|
|
|
|
pub const CF_RAW_OBJECTS: &str = "raw_objects";
|
|
pub const CF_RRDP_STATE: &str = "rrdp_state";
|
|
pub const CF_RRDP_OBJECT_INDEX: &str = "rrdp_object_index";
|
|
pub const CF_REPOSITORY_VIEW: &str = "repository_view";
|
|
pub const CF_RAW_BY_HASH: &str = "raw_by_hash";
|
|
pub const CF_VCIR: &str = "vcir";
|
|
pub const CF_AUDIT_RULE_INDEX: &str = "audit_rule_index";
|
|
pub const CF_RRDP_SOURCE: &str = "rrdp_source";
|
|
pub const CF_RRDP_SOURCE_MEMBER: &str = "rrdp_source_member";
|
|
pub const CF_RRDP_URI_OWNER: &str = "rrdp_uri_owner";
|
|
|
|
pub const ALL_COLUMN_FAMILY_NAMES: &[&str] = &[
|
|
CF_RAW_OBJECTS,
|
|
CF_RRDP_STATE,
|
|
CF_RRDP_OBJECT_INDEX,
|
|
CF_REPOSITORY_VIEW,
|
|
CF_RAW_BY_HASH,
|
|
CF_VCIR,
|
|
CF_AUDIT_RULE_INDEX,
|
|
CF_RRDP_SOURCE,
|
|
CF_RRDP_SOURCE_MEMBER,
|
|
CF_RRDP_URI_OWNER,
|
|
];
|
|
|
|
const RRDP_OBJECT_INDEX_PREFIX: &[u8] = b"rrdp_obj:";
|
|
const REPOSITORY_VIEW_KEY_PREFIX: &str = "repo_view:";
|
|
const RAW_BY_HASH_KEY_PREFIX: &str = "rawbyhash:";
|
|
const VCIR_KEY_PREFIX: &str = "vcir:";
|
|
const AUDIT_ROA_RULE_KEY_PREFIX: &str = "audit:roa_rule:";
|
|
const AUDIT_ASPA_RULE_KEY_PREFIX: &str = "audit:aspa_rule:";
|
|
const RRDP_SOURCE_KEY_PREFIX: &str = "rrdp_source:";
|
|
const RRDP_SOURCE_MEMBER_KEY_PREFIX: &str = "rrdp_source_member:";
|
|
const RRDP_URI_OWNER_KEY_PREFIX: &str = "rrdp_uri_owner:";
|
|
|
|
fn cf_opts() -> Options {
|
|
let mut opts = Options::default();
|
|
opts.set_compression_type(DBCompressionType::Lz4);
|
|
enable_blobdb_if_supported(&mut opts);
|
|
opts
|
|
}
|
|
|
|
pub fn column_family_descriptors() -> Vec<ColumnFamilyDescriptor> {
|
|
ALL_COLUMN_FAMILY_NAMES
|
|
.iter()
|
|
.map(|name| ColumnFamilyDescriptor::new(*name, cf_opts()))
|
|
.collect()
|
|
}
|
|
|
|
#[derive(Debug, thiserror::Error)]
|
|
pub enum StorageError {
|
|
#[error("rocksdb error: {0}")]
|
|
RocksDb(String),
|
|
|
|
#[error("missing column family: {0}")]
|
|
MissingColumnFamily(&'static str),
|
|
|
|
#[error("cbor codec error for {entity}: {detail}")]
|
|
Codec {
|
|
entity: &'static str,
|
|
detail: String,
|
|
},
|
|
|
|
#[error("invalid {entity}: {detail}")]
|
|
InvalidData {
|
|
entity: &'static str,
|
|
detail: String,
|
|
},
|
|
}
|
|
|
|
pub type StorageResult<T> = Result<T, StorageError>;
|
|
|
|
pub struct RocksStore {
|
|
db: DB,
|
|
}
|
|
|
|
pub mod pack {
|
|
pub use super::{PackFile, PackTime};
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum RrdpDeltaOp {
|
|
Upsert { rsync_uri: String, bytes: Vec<u8> },
|
|
Delete { rsync_uri: String },
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum RepositoryViewState {
|
|
Present,
|
|
Withdrawn,
|
|
Replaced,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct RepositoryViewEntry {
|
|
pub rsync_uri: String,
|
|
pub current_hash: Option<String>,
|
|
pub repository_source: Option<String>,
|
|
pub object_type: Option<String>,
|
|
pub state: RepositoryViewState,
|
|
}
|
|
|
|
impl RepositoryViewEntry {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_non_empty("repository_view.rsync_uri", &self.rsync_uri)?;
|
|
if let Some(source) = &self.repository_source {
|
|
validate_non_empty("repository_view.repository_source", source)?;
|
|
}
|
|
match self.state {
|
|
RepositoryViewState::Present | RepositoryViewState::Replaced => {
|
|
let hash = self
|
|
.current_hash
|
|
.as_deref()
|
|
.ok_or(StorageError::InvalidData {
|
|
entity: "repository_view",
|
|
detail: "current_hash is required when state is present or replaced"
|
|
.to_string(),
|
|
})?;
|
|
validate_sha256_hex("repository_view.current_hash", hash)?;
|
|
}
|
|
RepositoryViewState::Withdrawn => {
|
|
if let Some(hash) = &self.current_hash {
|
|
validate_sha256_hex("repository_view.current_hash", hash)?;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct RawByHashEntry {
|
|
pub sha256_hex: String,
|
|
pub bytes: Vec<u8>,
|
|
pub origin_uris: Vec<String>,
|
|
pub object_type: Option<String>,
|
|
pub encoding: Option<String>,
|
|
}
|
|
|
|
impl RawByHashEntry {
|
|
pub fn from_bytes(sha256_hex: impl Into<String>, bytes: Vec<u8>) -> Self {
|
|
Self {
|
|
sha256_hex: sha256_hex.into(),
|
|
bytes,
|
|
origin_uris: Vec::new(),
|
|
object_type: None,
|
|
encoding: None,
|
|
}
|
|
}
|
|
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_sha256_hex("raw_by_hash.sha256_hex", &self.sha256_hex)?;
|
|
if self.bytes.is_empty() {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "raw_by_hash",
|
|
detail: "bytes must not be empty".to_string(),
|
|
});
|
|
}
|
|
let computed = hex::encode(compute_sha256_32(&self.bytes));
|
|
if computed != self.sha256_hex.to_ascii_lowercase() {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "raw_by_hash",
|
|
detail: "sha256_hex does not match bytes".to_string(),
|
|
});
|
|
}
|
|
let mut seen = HashSet::with_capacity(self.origin_uris.len());
|
|
for uri in &self.origin_uris {
|
|
validate_non_empty("raw_by_hash.origin_uris[]", uri)?;
|
|
if !seen.insert(uri.as_str()) {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "raw_by_hash",
|
|
detail: format!("duplicate origin URI: {uri}"),
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ValidatedManifestMeta {
|
|
pub validated_manifest_number: Vec<u8>,
|
|
pub validated_manifest_this_update: PackTime,
|
|
pub validated_manifest_next_update: PackTime,
|
|
}
|
|
|
|
impl ValidatedManifestMeta {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_manifest_number_be(
|
|
"validated_manifest_meta.validated_manifest_number",
|
|
&self.validated_manifest_number,
|
|
)?;
|
|
let this_update = parse_time(
|
|
"validated_manifest_meta.validated_manifest_this_update",
|
|
&self.validated_manifest_this_update,
|
|
)?;
|
|
let next_update = parse_time(
|
|
"validated_manifest_meta.validated_manifest_next_update",
|
|
&self.validated_manifest_next_update,
|
|
)?;
|
|
if next_update < this_update {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "validated_manifest_meta",
|
|
detail: "validated_manifest_next_update must be >= validated_manifest_this_update"
|
|
.to_string(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct VcirInstanceGate {
|
|
pub manifest_next_update: PackTime,
|
|
pub current_crl_next_update: PackTime,
|
|
pub self_ca_not_after: PackTime,
|
|
pub instance_effective_until: PackTime,
|
|
}
|
|
|
|
impl VcirInstanceGate {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
let manifest_next_update = parse_time(
|
|
"vcir.instance_gate.manifest_next_update",
|
|
&self.manifest_next_update,
|
|
)?;
|
|
let current_crl_next_update = parse_time(
|
|
"vcir.instance_gate.current_crl_next_update",
|
|
&self.current_crl_next_update,
|
|
)?;
|
|
let self_ca_not_after = parse_time(
|
|
"vcir.instance_gate.self_ca_not_after",
|
|
&self.self_ca_not_after,
|
|
)?;
|
|
let instance_effective_until = parse_time(
|
|
"vcir.instance_gate.instance_effective_until",
|
|
&self.instance_effective_until,
|
|
)?;
|
|
let expected = manifest_next_update
|
|
.min(current_crl_next_update)
|
|
.min(self_ca_not_after);
|
|
if instance_effective_until != expected {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "vcir.instance_gate",
|
|
detail: "instance_effective_until must equal min(manifest_next_update, current_crl_next_update, self_ca_not_after)".to_string(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct VcirChildEntry {
|
|
pub child_manifest_rsync_uri: String,
|
|
pub child_cert_rsync_uri: String,
|
|
pub child_cert_hash: String,
|
|
pub child_ski: String,
|
|
pub child_rsync_base_uri: String,
|
|
pub child_publication_point_rsync_uri: String,
|
|
pub child_rrdp_notification_uri: Option<String>,
|
|
pub child_effective_ip_resources: Option<IpResourceSet>,
|
|
pub child_effective_as_resources: Option<AsResourceSet>,
|
|
pub accepted_at_validation_time: PackTime,
|
|
}
|
|
|
|
impl VcirChildEntry {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_non_empty(
|
|
"vcir.child_entries[].child_manifest_rsync_uri",
|
|
&self.child_manifest_rsync_uri,
|
|
)?;
|
|
validate_non_empty(
|
|
"vcir.child_entries[].child_cert_rsync_uri",
|
|
&self.child_cert_rsync_uri,
|
|
)?;
|
|
validate_sha256_hex(
|
|
"vcir.child_entries[].child_cert_hash",
|
|
&self.child_cert_hash,
|
|
)?;
|
|
validate_non_empty("vcir.child_entries[].child_ski", &self.child_ski)?;
|
|
validate_non_empty(
|
|
"vcir.child_entries[].child_rsync_base_uri",
|
|
&self.child_rsync_base_uri,
|
|
)?;
|
|
validate_non_empty(
|
|
"vcir.child_entries[].child_publication_point_rsync_uri",
|
|
&self.child_publication_point_rsync_uri,
|
|
)?;
|
|
if let Some(uri) = &self.child_rrdp_notification_uri {
|
|
validate_non_empty("vcir.child_entries[].child_rrdp_notification_uri", uri)?;
|
|
}
|
|
parse_time(
|
|
"vcir.child_entries[].accepted_at_validation_time",
|
|
&self.accepted_at_validation_time,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum VcirOutputType {
|
|
Vrp,
|
|
Aspa,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct VcirLocalOutput {
|
|
pub output_id: String,
|
|
pub output_type: VcirOutputType,
|
|
pub item_effective_until: PackTime,
|
|
pub source_object_uri: String,
|
|
pub source_object_type: String,
|
|
pub source_object_hash: String,
|
|
pub source_ee_cert_hash: String,
|
|
pub payload_json: String,
|
|
pub rule_hash: String,
|
|
pub validation_path_hint: Vec<String>,
|
|
}
|
|
|
|
impl VcirLocalOutput {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_non_empty("vcir.local_outputs[].output_id", &self.output_id)?;
|
|
parse_time(
|
|
"vcir.local_outputs[].item_effective_until",
|
|
&self.item_effective_until,
|
|
)?;
|
|
validate_non_empty(
|
|
"vcir.local_outputs[].source_object_uri",
|
|
&self.source_object_uri,
|
|
)?;
|
|
validate_non_empty(
|
|
"vcir.local_outputs[].source_object_type",
|
|
&self.source_object_type,
|
|
)?;
|
|
validate_sha256_hex(
|
|
"vcir.local_outputs[].source_object_hash",
|
|
&self.source_object_hash,
|
|
)?;
|
|
validate_sha256_hex(
|
|
"vcir.local_outputs[].source_ee_cert_hash",
|
|
&self.source_ee_cert_hash,
|
|
)?;
|
|
validate_sha256_hex("vcir.local_outputs[].rule_hash", &self.rule_hash)?;
|
|
validate_non_empty("vcir.local_outputs[].payload_json", &self.payload_json)?;
|
|
for hint in &self.validation_path_hint {
|
|
validate_non_empty("vcir.local_outputs[].validation_path_hint[]", hint)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum VcirArtifactRole {
|
|
Manifest,
|
|
CurrentCrl,
|
|
ChildCaCert,
|
|
SignedObject,
|
|
EeCert,
|
|
IssuerCert,
|
|
Tal,
|
|
TrustAnchorCert,
|
|
Other,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum VcirArtifactKind {
|
|
Cer,
|
|
Crl,
|
|
Mft,
|
|
Roa,
|
|
Aspa,
|
|
Gbr,
|
|
Tal,
|
|
Other,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum VcirArtifactValidationStatus {
|
|
Accepted,
|
|
Rejected,
|
|
WarningOnly,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct VcirRelatedArtifact {
|
|
pub artifact_role: VcirArtifactRole,
|
|
pub artifact_kind: VcirArtifactKind,
|
|
pub uri: Option<String>,
|
|
pub sha256: String,
|
|
pub object_type: Option<String>,
|
|
pub validation_status: VcirArtifactValidationStatus,
|
|
}
|
|
|
|
impl VcirRelatedArtifact {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
if let Some(uri) = &self.uri {
|
|
validate_non_empty("vcir.related_artifacts[].uri", uri)?;
|
|
}
|
|
validate_sha256_hex("vcir.related_artifacts[].sha256", &self.sha256)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct VcirSummary {
|
|
pub local_vrp_count: u32,
|
|
pub local_aspa_count: u32,
|
|
pub child_count: u32,
|
|
pub accepted_object_count: u32,
|
|
pub rejected_object_count: u32,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct VcirAuditSummary {
|
|
pub failed_fetch_eligible: bool,
|
|
pub last_failed_fetch_reason: Option<String>,
|
|
pub warning_count: u32,
|
|
pub audit_flags: Vec<String>,
|
|
}
|
|
|
|
impl VcirAuditSummary {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
if let Some(reason) = &self.last_failed_fetch_reason {
|
|
validate_non_empty("vcir.audit_summary.last_failed_fetch_reason", reason)?;
|
|
}
|
|
for flag in &self.audit_flags {
|
|
validate_non_empty("vcir.audit_summary.audit_flags[]", flag)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ValidatedCaInstanceResult {
|
|
pub manifest_rsync_uri: String,
|
|
pub parent_manifest_rsync_uri: Option<String>,
|
|
pub tal_id: String,
|
|
pub ca_subject_name: String,
|
|
pub ca_ski: String,
|
|
pub issuer_ski: String,
|
|
pub last_successful_validation_time: PackTime,
|
|
pub current_manifest_rsync_uri: String,
|
|
pub current_crl_rsync_uri: String,
|
|
pub validated_manifest_meta: ValidatedManifestMeta,
|
|
pub instance_gate: VcirInstanceGate,
|
|
pub child_entries: Vec<VcirChildEntry>,
|
|
pub local_outputs: Vec<VcirLocalOutput>,
|
|
pub related_artifacts: Vec<VcirRelatedArtifact>,
|
|
pub summary: VcirSummary,
|
|
pub audit_summary: VcirAuditSummary,
|
|
}
|
|
|
|
impl ValidatedCaInstanceResult {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_non_empty("vcir.manifest_rsync_uri", &self.manifest_rsync_uri)?;
|
|
if let Some(parent_manifest_rsync_uri) = &self.parent_manifest_rsync_uri {
|
|
validate_non_empty("vcir.parent_manifest_rsync_uri", parent_manifest_rsync_uri)?;
|
|
}
|
|
validate_non_empty("vcir.tal_id", &self.tal_id)?;
|
|
validate_non_empty("vcir.ca_subject_name", &self.ca_subject_name)?;
|
|
validate_non_empty("vcir.ca_ski", &self.ca_ski)?;
|
|
validate_non_empty("vcir.issuer_ski", &self.issuer_ski)?;
|
|
parse_time(
|
|
"vcir.last_successful_validation_time",
|
|
&self.last_successful_validation_time,
|
|
)?;
|
|
validate_non_empty(
|
|
"vcir.current_manifest_rsync_uri",
|
|
&self.current_manifest_rsync_uri,
|
|
)?;
|
|
validate_non_empty("vcir.current_crl_rsync_uri", &self.current_crl_rsync_uri)?;
|
|
self.validated_manifest_meta.validate_internal()?;
|
|
self.instance_gate.validate_internal()?;
|
|
|
|
let expected_manifest_next = self
|
|
.validated_manifest_meta
|
|
.validated_manifest_next_update
|
|
.parse()
|
|
.map_err(|detail| StorageError::InvalidData {
|
|
entity: "vcir",
|
|
detail: format!(
|
|
"validated_manifest_meta.validated_manifest_next_update invalid: {detail}"
|
|
),
|
|
})?;
|
|
let instance_manifest_next =
|
|
self.instance_gate
|
|
.manifest_next_update
|
|
.parse()
|
|
.map_err(|detail| StorageError::InvalidData {
|
|
entity: "vcir",
|
|
detail: format!("instance_gate.manifest_next_update invalid: {detail}"),
|
|
})?;
|
|
if expected_manifest_next != instance_manifest_next {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "vcir",
|
|
detail: "instance_gate.manifest_next_update must equal validated_manifest_meta.validated_manifest_next_update".to_string(),
|
|
});
|
|
}
|
|
|
|
let mut child_manifests = HashSet::with_capacity(self.child_entries.len());
|
|
for child in &self.child_entries {
|
|
child.validate_internal()?;
|
|
if !child_manifests.insert(child.child_manifest_rsync_uri.as_str()) {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "vcir",
|
|
detail: format!(
|
|
"duplicate child_manifest_rsync_uri: {}",
|
|
child.child_manifest_rsync_uri
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
let mut output_ids = HashSet::with_capacity(self.local_outputs.len());
|
|
let mut vrp_count = 0u32;
|
|
let mut aspa_count = 0u32;
|
|
for output in &self.local_outputs {
|
|
output.validate_internal()?;
|
|
if !output_ids.insert(output.output_id.as_str()) {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "vcir",
|
|
detail: format!("duplicate output_id: {}", output.output_id),
|
|
});
|
|
}
|
|
match output.output_type {
|
|
VcirOutputType::Vrp => vrp_count += 1,
|
|
VcirOutputType::Aspa => aspa_count += 1,
|
|
}
|
|
}
|
|
if self.summary.local_vrp_count != vrp_count {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "vcir.summary",
|
|
detail: format!(
|
|
"local_vrp_count={} does not match local_outputs count {}",
|
|
self.summary.local_vrp_count, vrp_count
|
|
),
|
|
});
|
|
}
|
|
if self.summary.local_aspa_count != aspa_count {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "vcir.summary",
|
|
detail: format!(
|
|
"local_aspa_count={} does not match local_outputs count {}",
|
|
self.summary.local_aspa_count, aspa_count
|
|
),
|
|
});
|
|
}
|
|
if self.summary.child_count != self.child_entries.len() as u32 {
|
|
return Err(StorageError::InvalidData {
|
|
entity: "vcir.summary",
|
|
detail: format!(
|
|
"child_count={} does not match child_entries length {}",
|
|
self.summary.child_count,
|
|
self.child_entries.len()
|
|
),
|
|
});
|
|
}
|
|
|
|
for artifact in &self.related_artifacts {
|
|
artifact.validate_internal()?;
|
|
}
|
|
self.audit_summary.validate_internal()?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum AuditRuleKind {
|
|
Roa,
|
|
Aspa,
|
|
}
|
|
|
|
impl AuditRuleKind {
|
|
fn key_prefix(self) -> &'static str {
|
|
match self {
|
|
Self::Roa => AUDIT_ROA_RULE_KEY_PREFIX,
|
|
Self::Aspa => AUDIT_ASPA_RULE_KEY_PREFIX,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct AuditRuleIndexEntry {
|
|
pub kind: AuditRuleKind,
|
|
pub rule_hash: String,
|
|
pub manifest_rsync_uri: String,
|
|
pub source_object_uri: String,
|
|
pub source_object_hash: String,
|
|
pub output_id: String,
|
|
pub item_effective_until: PackTime,
|
|
}
|
|
|
|
impl AuditRuleIndexEntry {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_sha256_hex("audit_rule_index.rule_hash", &self.rule_hash)?;
|
|
validate_non_empty(
|
|
"audit_rule_index.manifest_rsync_uri",
|
|
&self.manifest_rsync_uri,
|
|
)?;
|
|
validate_non_empty(
|
|
"audit_rule_index.source_object_uri",
|
|
&self.source_object_uri,
|
|
)?;
|
|
validate_sha256_hex(
|
|
"audit_rule_index.source_object_hash",
|
|
&self.source_object_hash,
|
|
)?;
|
|
validate_non_empty("audit_rule_index.output_id", &self.output_id)?;
|
|
parse_time(
|
|
"audit_rule_index.item_effective_until",
|
|
&self.item_effective_until,
|
|
)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum RrdpSourceSyncState {
|
|
Empty,
|
|
SnapshotOnly,
|
|
DeltaReady,
|
|
Error,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct RrdpSourceRecord {
|
|
pub notify_uri: String,
|
|
pub last_session_id: Option<String>,
|
|
pub last_serial: Option<u64>,
|
|
pub first_seen_at: PackTime,
|
|
pub last_seen_at: PackTime,
|
|
pub last_sync_at: Option<PackTime>,
|
|
pub sync_state: RrdpSourceSyncState,
|
|
pub last_snapshot_uri: Option<String>,
|
|
pub last_snapshot_hash: Option<String>,
|
|
pub last_error: Option<String>,
|
|
}
|
|
|
|
impl RrdpSourceRecord {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_non_empty("rrdp_source.notify_uri", &self.notify_uri)?;
|
|
if let Some(session_id) = &self.last_session_id {
|
|
validate_non_empty("rrdp_source.last_session_id", session_id)?;
|
|
}
|
|
parse_time("rrdp_source.first_seen_at", &self.first_seen_at)?;
|
|
parse_time("rrdp_source.last_seen_at", &self.last_seen_at)?;
|
|
if let Some(last_sync_at) = &self.last_sync_at {
|
|
parse_time("rrdp_source.last_sync_at", last_sync_at)?;
|
|
}
|
|
if let Some(last_snapshot_uri) = &self.last_snapshot_uri {
|
|
validate_non_empty("rrdp_source.last_snapshot_uri", last_snapshot_uri)?;
|
|
}
|
|
if let Some(last_snapshot_hash) = &self.last_snapshot_hash {
|
|
validate_sha256_hex("rrdp_source.last_snapshot_hash", last_snapshot_hash)?;
|
|
}
|
|
if let Some(last_error) = &self.last_error {
|
|
validate_non_empty("rrdp_source.last_error", last_error)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct RrdpSourceMemberRecord {
|
|
pub notify_uri: String,
|
|
pub rsync_uri: String,
|
|
pub current_hash: Option<String>,
|
|
pub object_type: Option<String>,
|
|
pub present: bool,
|
|
pub last_confirmed_session_id: String,
|
|
pub last_confirmed_serial: u64,
|
|
pub last_changed_at: PackTime,
|
|
}
|
|
|
|
impl RrdpSourceMemberRecord {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_non_empty("rrdp_source_member.notify_uri", &self.notify_uri)?;
|
|
validate_non_empty("rrdp_source_member.rsync_uri", &self.rsync_uri)?;
|
|
validate_non_empty(
|
|
"rrdp_source_member.last_confirmed_session_id",
|
|
&self.last_confirmed_session_id,
|
|
)?;
|
|
if self.present {
|
|
let hash = self
|
|
.current_hash
|
|
.as_deref()
|
|
.ok_or(StorageError::InvalidData {
|
|
entity: "rrdp_source_member",
|
|
detail: "current_hash is required when present=true".to_string(),
|
|
})?;
|
|
validate_sha256_hex("rrdp_source_member.current_hash", hash)?;
|
|
} else if let Some(hash) = &self.current_hash {
|
|
validate_sha256_hex("rrdp_source_member.current_hash", hash)?;
|
|
}
|
|
parse_time("rrdp_source_member.last_changed_at", &self.last_changed_at)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum RrdpUriOwnerState {
|
|
Active,
|
|
Conflict,
|
|
Withdrawn,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct RrdpUriOwnerRecord {
|
|
pub rsync_uri: String,
|
|
pub notify_uri: String,
|
|
pub current_hash: Option<String>,
|
|
pub last_confirmed_session_id: String,
|
|
pub last_confirmed_serial: u64,
|
|
pub last_changed_at: PackTime,
|
|
pub owner_state: RrdpUriOwnerState,
|
|
}
|
|
|
|
impl RrdpUriOwnerRecord {
|
|
pub fn validate_internal(&self) -> StorageResult<()> {
|
|
validate_non_empty("rrdp_uri_owner.rsync_uri", &self.rsync_uri)?;
|
|
validate_non_empty("rrdp_uri_owner.notify_uri", &self.notify_uri)?;
|
|
validate_non_empty(
|
|
"rrdp_uri_owner.last_confirmed_session_id",
|
|
&self.last_confirmed_session_id,
|
|
)?;
|
|
if let Some(hash) = &self.current_hash {
|
|
validate_sha256_hex("rrdp_uri_owner.current_hash", hash)?;
|
|
}
|
|
parse_time("rrdp_uri_owner.last_changed_at", &self.last_changed_at)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl RocksStore {
|
|
pub fn open(path: &Path) -> StorageResult<Self> {
|
|
let mut base_opts = Options::default();
|
|
base_opts.create_if_missing(true);
|
|
base_opts.create_missing_column_families(true);
|
|
base_opts.set_compression_type(DBCompressionType::Lz4);
|
|
enable_blobdb_if_supported(&mut base_opts);
|
|
|
|
let db = DB::open_cf_descriptors(&base_opts, path, column_family_descriptors())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
|
|
Ok(Self { db })
|
|
}
|
|
|
|
fn cf(&self, name: &'static str) -> StorageResult<&ColumnFamily> {
|
|
self.db
|
|
.cf_handle(name)
|
|
.ok_or(StorageError::MissingColumnFamily(name))
|
|
}
|
|
|
|
pub fn put_raw(&self, rsync_uri: &str, bytes: &[u8]) -> StorageResult<()> {
|
|
let cf = self.cf(CF_RAW_OBJECTS)?;
|
|
self.db
|
|
.put_cf(cf, rsync_uri.as_bytes(), bytes)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn put_raw_batch(&self, objects: Vec<(String, Vec<u8>)>) -> StorageResult<usize> {
|
|
if objects.is_empty() {
|
|
return Ok(0);
|
|
}
|
|
|
|
let cf = self.cf(CF_RAW_OBJECTS)?;
|
|
let mut batch = WriteBatch::default();
|
|
for (rsync_uri, bytes) in &objects {
|
|
batch.put_cf(cf, rsync_uri.as_bytes(), bytes.as_slice());
|
|
}
|
|
self.write_batch(batch)?;
|
|
Ok(objects.len())
|
|
}
|
|
|
|
pub fn get_raw(&self, rsync_uri: &str) -> StorageResult<Option<Vec<u8>>> {
|
|
let cf = self.cf(CF_RAW_OBJECTS)?;
|
|
let v = self
|
|
.db
|
|
.get_cf(cf, rsync_uri.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(v)
|
|
}
|
|
|
|
pub fn delete_raw(&self, rsync_uri: &str) -> StorageResult<()> {
|
|
let cf = self.cf(CF_RAW_OBJECTS)?;
|
|
self.db
|
|
.delete_cf(cf, rsync_uri.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn put_rrdp_state(&self, notification_uri: &str, bytes: &[u8]) -> StorageResult<()> {
|
|
let cf = self.cf(CF_RRDP_STATE)?;
|
|
self.db
|
|
.put_cf(cf, notification_uri.as_bytes(), bytes)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_rrdp_state(&self, notification_uri: &str) -> StorageResult<Option<Vec<u8>>> {
|
|
let cf = self.cf(CF_RRDP_STATE)?;
|
|
let v = self
|
|
.db
|
|
.get_cf(cf, notification_uri.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(v)
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn delete_rrdp_state(&self, notification_uri: &str) -> StorageResult<()> {
|
|
let cf = self.cf(CF_RRDP_STATE)?;
|
|
self.db
|
|
.delete_cf(cf, notification_uri.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
fn rrdp_object_index_key(notification_uri: &str, rsync_uri: &str) -> Vec<u8> {
|
|
let mut out = Vec::with_capacity(
|
|
RRDP_OBJECT_INDEX_PREFIX.len() + notification_uri.len() + 1 + rsync_uri.len(),
|
|
);
|
|
out.extend_from_slice(RRDP_OBJECT_INDEX_PREFIX);
|
|
out.extend_from_slice(notification_uri.as_bytes());
|
|
out.push(0);
|
|
out.extend_from_slice(rsync_uri.as_bytes());
|
|
out
|
|
}
|
|
|
|
fn rrdp_object_index_prefix(notification_uri: &str) -> Vec<u8> {
|
|
let mut out =
|
|
Vec::with_capacity(RRDP_OBJECT_INDEX_PREFIX.len() + notification_uri.len() + 1);
|
|
out.extend_from_slice(RRDP_OBJECT_INDEX_PREFIX);
|
|
out.extend_from_slice(notification_uri.as_bytes());
|
|
out.push(0);
|
|
out
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn rrdp_object_index_contains(
|
|
&self,
|
|
notification_uri: &str,
|
|
rsync_uri: &str,
|
|
) -> StorageResult<bool> {
|
|
let cf = self.cf(CF_RRDP_OBJECT_INDEX)?;
|
|
let k = Self::rrdp_object_index_key(notification_uri, rsync_uri);
|
|
Ok(self
|
|
.db
|
|
.get_cf(cf, k)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
|
.is_some())
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn rrdp_object_index_iter(
|
|
&self,
|
|
notification_uri: &str,
|
|
) -> StorageResult<impl Iterator<Item = String> + '_> {
|
|
let cf = self.cf(CF_RRDP_OBJECT_INDEX)?;
|
|
let prefix = Self::rrdp_object_index_prefix(notification_uri);
|
|
let prefix_len = prefix.len();
|
|
let mode = IteratorMode::From(prefix.as_slice(), Direction::Forward);
|
|
Ok(self
|
|
.db
|
|
.iterator_cf(cf, mode)
|
|
.take_while(move |res| match res {
|
|
Ok((k, _v)) => k.starts_with(prefix.as_slice()),
|
|
Err(_) => false,
|
|
})
|
|
.filter_map(move |res| {
|
|
let (k, _v) = res.ok()?;
|
|
let rsync_part = k.get(prefix_len..)?;
|
|
let s = std::str::from_utf8(rsync_part).ok()?;
|
|
Some(s.to_string())
|
|
}))
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn rrdp_object_index_clear(&self, notification_uri: &str) -> StorageResult<usize> {
|
|
let cf = self.cf(CF_RRDP_OBJECT_INDEX)?;
|
|
let prefix = Self::rrdp_object_index_prefix(notification_uri);
|
|
let mode = IteratorMode::From(prefix.as_slice(), Direction::Forward);
|
|
let keys: Vec<Box<[u8]>> = self
|
|
.db
|
|
.iterator_cf(cf, mode)
|
|
.take_while(|res| match res {
|
|
Ok((k, _v)) => k.starts_with(prefix.as_slice()),
|
|
Err(_) => false,
|
|
})
|
|
.filter_map(|res| res.ok().map(|(k, _v)| k))
|
|
.collect();
|
|
|
|
if keys.is_empty() {
|
|
return Ok(0);
|
|
}
|
|
|
|
let mut batch = WriteBatch::default();
|
|
for k in &keys {
|
|
batch.delete_cf(cf, k);
|
|
}
|
|
self.write_batch(batch)?;
|
|
Ok(keys.len())
|
|
}
|
|
|
|
pub fn apply_rrdp_snapshot(
|
|
&self,
|
|
notification_uri: &str,
|
|
published: &[(String, Vec<u8>)],
|
|
) -> StorageResult<usize> {
|
|
let raw_cf = self.cf(CF_RAW_OBJECTS)?;
|
|
let idx_cf = self.cf(CF_RRDP_OBJECT_INDEX)?;
|
|
|
|
let mut new_set: HashSet<&str> = HashSet::with_capacity(published.len());
|
|
for (u, _b) in published {
|
|
new_set.insert(u.as_str());
|
|
}
|
|
|
|
let old_uris: Vec<String> = self.rrdp_object_index_iter(notification_uri)?.collect();
|
|
|
|
let mut batch = WriteBatch::default();
|
|
|
|
for old in &old_uris {
|
|
if !new_set.contains(old.as_str()) {
|
|
batch.delete_cf(raw_cf, old.as_bytes());
|
|
let k = Self::rrdp_object_index_key(notification_uri, old.as_str());
|
|
batch.delete_cf(idx_cf, k);
|
|
}
|
|
}
|
|
|
|
for (uri, bytes) in published {
|
|
batch.put_cf(raw_cf, uri.as_bytes(), bytes.as_slice());
|
|
let k = Self::rrdp_object_index_key(notification_uri, uri.as_str());
|
|
batch.put_cf(idx_cf, k, b"");
|
|
}
|
|
|
|
self.write_batch(batch)?;
|
|
Ok(published.len())
|
|
}
|
|
|
|
pub fn apply_rrdp_delta(
|
|
&self,
|
|
notification_uri: &str,
|
|
ops: &[RrdpDeltaOp],
|
|
) -> StorageResult<usize> {
|
|
if ops.is_empty() {
|
|
return Ok(0);
|
|
}
|
|
|
|
let raw_cf = self.cf(CF_RAW_OBJECTS)?;
|
|
let idx_cf = self.cf(CF_RRDP_OBJECT_INDEX)?;
|
|
|
|
let mut batch = WriteBatch::default();
|
|
for op in ops {
|
|
match op {
|
|
RrdpDeltaOp::Upsert { rsync_uri, bytes } => {
|
|
batch.put_cf(raw_cf, rsync_uri.as_bytes(), bytes.as_slice());
|
|
let k = Self::rrdp_object_index_key(notification_uri, rsync_uri.as_str());
|
|
batch.put_cf(idx_cf, k, b"");
|
|
}
|
|
RrdpDeltaOp::Delete { rsync_uri } => {
|
|
batch.delete_cf(raw_cf, rsync_uri.as_bytes());
|
|
let k = Self::rrdp_object_index_key(notification_uri, rsync_uri.as_str());
|
|
batch.delete_cf(idx_cf, k);
|
|
}
|
|
}
|
|
}
|
|
|
|
self.write_batch(batch)?;
|
|
Ok(ops.len())
|
|
}
|
|
|
|
pub fn put_repository_view_entry(&self, entry: &RepositoryViewEntry) -> StorageResult<()> {
|
|
entry.validate_internal()?;
|
|
let cf = self.cf(CF_REPOSITORY_VIEW)?;
|
|
let key = repository_view_key(&entry.rsync_uri);
|
|
let value = encode_cbor(entry, "repository_view")?;
|
|
self.db
|
|
.put_cf(cf, key.as_bytes(), value)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_repository_view_entry(
|
|
&self,
|
|
rsync_uri: &str,
|
|
) -> StorageResult<Option<RepositoryViewEntry>> {
|
|
let cf = self.cf(CF_REPOSITORY_VIEW)?;
|
|
let key = repository_view_key(rsync_uri);
|
|
let Some(bytes) = self
|
|
.db
|
|
.get_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let entry = decode_cbor::<RepositoryViewEntry>(&bytes, "repository_view")?;
|
|
entry.validate_internal()?;
|
|
Ok(Some(entry))
|
|
}
|
|
|
|
pub fn delete_repository_view_entry(&self, rsync_uri: &str) -> StorageResult<()> {
|
|
let cf = self.cf(CF_REPOSITORY_VIEW)?;
|
|
let key = repository_view_key(rsync_uri);
|
|
self.db
|
|
.delete_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn list_repository_view_entries_with_prefix(
|
|
&self,
|
|
rsync_uri_prefix: &str,
|
|
) -> StorageResult<Vec<RepositoryViewEntry>> {
|
|
let cf = self.cf(CF_REPOSITORY_VIEW)?;
|
|
let prefix = repository_view_prefix(rsync_uri_prefix);
|
|
let mode = IteratorMode::From(prefix.as_bytes(), Direction::Forward);
|
|
self.db
|
|
.iterator_cf(cf, mode)
|
|
.take_while(|res| match res {
|
|
Ok((key, _)) => key.starts_with(prefix.as_bytes()),
|
|
Err(_) => false,
|
|
})
|
|
.map(|res| {
|
|
let (_key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
let entry = decode_cbor::<RepositoryViewEntry>(&value, "repository_view")?;
|
|
entry.validate_internal()?;
|
|
Ok(entry)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn put_raw_by_hash_entry(&self, entry: &RawByHashEntry) -> StorageResult<()> {
|
|
entry.validate_internal()?;
|
|
let cf = self.cf(CF_RAW_BY_HASH)?;
|
|
let key = raw_by_hash_key(&entry.sha256_hex);
|
|
let value = encode_cbor(entry, "raw_by_hash")?;
|
|
self.db
|
|
.put_cf(cf, key.as_bytes(), value)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_raw_by_hash_entry(&self, sha256_hex: &str) -> StorageResult<Option<RawByHashEntry>> {
|
|
let cf = self.cf(CF_RAW_BY_HASH)?;
|
|
let key = raw_by_hash_key(sha256_hex);
|
|
let Some(bytes) = self
|
|
.db
|
|
.get_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let entry = decode_cbor::<RawByHashEntry>(&bytes, "raw_by_hash")?;
|
|
entry.validate_internal()?;
|
|
Ok(Some(entry))
|
|
}
|
|
|
|
pub fn put_vcir(&self, vcir: &ValidatedCaInstanceResult) -> StorageResult<()> {
|
|
vcir.validate_internal()?;
|
|
let cf = self.cf(CF_VCIR)?;
|
|
let key = vcir_key(&vcir.manifest_rsync_uri);
|
|
let value = encode_cbor(vcir, "vcir")?;
|
|
self.db
|
|
.put_cf(cf, key.as_bytes(), value)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_vcir(
|
|
&self,
|
|
manifest_rsync_uri: &str,
|
|
) -> StorageResult<Option<ValidatedCaInstanceResult>> {
|
|
let cf = self.cf(CF_VCIR)?;
|
|
let key = vcir_key(manifest_rsync_uri);
|
|
let Some(bytes) = self
|
|
.db
|
|
.get_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let vcir = decode_cbor::<ValidatedCaInstanceResult>(&bytes, "vcir")?;
|
|
vcir.validate_internal()?;
|
|
Ok(Some(vcir))
|
|
}
|
|
|
|
pub fn delete_vcir(&self, manifest_rsync_uri: &str) -> StorageResult<()> {
|
|
let cf = self.cf(CF_VCIR)?;
|
|
let key = vcir_key(manifest_rsync_uri);
|
|
self.db
|
|
.delete_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn put_audit_rule_index_entry(&self, entry: &AuditRuleIndexEntry) -> StorageResult<()> {
|
|
entry.validate_internal()?;
|
|
let cf = self.cf(CF_AUDIT_RULE_INDEX)?;
|
|
let key = audit_rule_key(entry.kind, &entry.rule_hash);
|
|
let value = encode_cbor(entry, "audit_rule_index")?;
|
|
self.db
|
|
.put_cf(cf, key.as_bytes(), value)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_audit_rule_index_entry(
|
|
&self,
|
|
kind: AuditRuleKind,
|
|
rule_hash: &str,
|
|
) -> StorageResult<Option<AuditRuleIndexEntry>> {
|
|
let cf = self.cf(CF_AUDIT_RULE_INDEX)?;
|
|
let key = audit_rule_key(kind, rule_hash);
|
|
let Some(bytes) = self
|
|
.db
|
|
.get_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let entry = decode_cbor::<AuditRuleIndexEntry>(&bytes, "audit_rule_index")?;
|
|
entry.validate_internal()?;
|
|
Ok(Some(entry))
|
|
}
|
|
|
|
pub fn delete_audit_rule_index_entry(
|
|
&self,
|
|
kind: AuditRuleKind,
|
|
rule_hash: &str,
|
|
) -> StorageResult<()> {
|
|
let cf = self.cf(CF_AUDIT_RULE_INDEX)?;
|
|
let key = audit_rule_key(kind, rule_hash);
|
|
self.db
|
|
.delete_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn put_rrdp_source_record(&self, record: &RrdpSourceRecord) -> StorageResult<()> {
|
|
record.validate_internal()?;
|
|
let cf = self.cf(CF_RRDP_SOURCE)?;
|
|
let key = rrdp_source_key(&record.notify_uri);
|
|
let value = encode_cbor(record, "rrdp_source")?;
|
|
self.db
|
|
.put_cf(cf, key.as_bytes(), value)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_rrdp_source_record(
|
|
&self,
|
|
notify_uri: &str,
|
|
) -> StorageResult<Option<RrdpSourceRecord>> {
|
|
let cf = self.cf(CF_RRDP_SOURCE)?;
|
|
let key = rrdp_source_key(notify_uri);
|
|
let Some(bytes) = self
|
|
.db
|
|
.get_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let record = decode_cbor::<RrdpSourceRecord>(&bytes, "rrdp_source")?;
|
|
record.validate_internal()?;
|
|
Ok(Some(record))
|
|
}
|
|
|
|
pub fn put_rrdp_source_member_record(
|
|
&self,
|
|
record: &RrdpSourceMemberRecord,
|
|
) -> StorageResult<()> {
|
|
record.validate_internal()?;
|
|
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
|
|
let key = rrdp_source_member_key(&record.notify_uri, &record.rsync_uri);
|
|
let value = encode_cbor(record, "rrdp_source_member")?;
|
|
self.db
|
|
.put_cf(cf, key.as_bytes(), value)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_rrdp_source_member_record(
|
|
&self,
|
|
notify_uri: &str,
|
|
rsync_uri: &str,
|
|
) -> StorageResult<Option<RrdpSourceMemberRecord>> {
|
|
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
|
|
let key = rrdp_source_member_key(notify_uri, rsync_uri);
|
|
let Some(bytes) = self
|
|
.db
|
|
.get_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let record = decode_cbor::<RrdpSourceMemberRecord>(&bytes, "rrdp_source_member")?;
|
|
record.validate_internal()?;
|
|
Ok(Some(record))
|
|
}
|
|
|
|
pub fn list_rrdp_source_member_records(
|
|
&self,
|
|
notify_uri: &str,
|
|
) -> StorageResult<Vec<RrdpSourceMemberRecord>> {
|
|
let cf = self.cf(CF_RRDP_SOURCE_MEMBER)?;
|
|
let prefix = rrdp_source_member_prefix(notify_uri);
|
|
let mode = IteratorMode::From(prefix.as_bytes(), Direction::Forward);
|
|
self.db
|
|
.iterator_cf(cf, mode)
|
|
.take_while(|res| match res {
|
|
Ok((key, _)) => key.starts_with(prefix.as_bytes()),
|
|
Err(_) => false,
|
|
})
|
|
.map(|res| {
|
|
let (_key, value) = res.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
let record = decode_cbor::<RrdpSourceMemberRecord>(&value, "rrdp_source_member")?;
|
|
record.validate_internal()?;
|
|
Ok(record)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
pub fn put_rrdp_uri_owner_record(&self, record: &RrdpUriOwnerRecord) -> StorageResult<()> {
|
|
record.validate_internal()?;
|
|
let cf = self.cf(CF_RRDP_URI_OWNER)?;
|
|
let key = rrdp_uri_owner_key(&record.rsync_uri);
|
|
let value = encode_cbor(record, "rrdp_uri_owner")?;
|
|
self.db
|
|
.put_cf(cf, key.as_bytes(), value)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn get_rrdp_uri_owner_record(
|
|
&self,
|
|
rsync_uri: &str,
|
|
) -> StorageResult<Option<RrdpUriOwnerRecord>> {
|
|
let cf = self.cf(CF_RRDP_URI_OWNER)?;
|
|
let key = rrdp_uri_owner_key(rsync_uri);
|
|
let Some(bytes) = self
|
|
.db
|
|
.get_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?
|
|
else {
|
|
return Ok(None);
|
|
};
|
|
let record = decode_cbor::<RrdpUriOwnerRecord>(&bytes, "rrdp_uri_owner")?;
|
|
record.validate_internal()?;
|
|
Ok(Some(record))
|
|
}
|
|
|
|
pub fn delete_rrdp_uri_owner_record(&self, rsync_uri: &str) -> StorageResult<()> {
|
|
let cf = self.cf(CF_RRDP_URI_OWNER)?;
|
|
let key = rrdp_uri_owner_key(rsync_uri);
|
|
self.db
|
|
.delete_cf(cf, key.as_bytes())
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn raw_iter_prefix<'a>(
|
|
&'a self,
|
|
prefix: &'a [u8],
|
|
) -> StorageResult<impl Iterator<Item = (Box<[u8]>, Box<[u8]>)> + 'a> {
|
|
let cf = self.cf(CF_RAW_OBJECTS)?;
|
|
let mode = IteratorMode::From(prefix, Direction::Forward);
|
|
Ok(self
|
|
.db
|
|
.iterator_cf(cf, mode)
|
|
.take_while(move |res| match res {
|
|
Ok((k, _v)) => k.starts_with(prefix),
|
|
Err(_) => false,
|
|
})
|
|
.filter_map(|res| res.ok()))
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
pub fn raw_iter_all<'a>(
|
|
&'a self,
|
|
) -> StorageResult<impl Iterator<Item = (Box<[u8]>, Box<[u8]>)> + 'a> {
|
|
let cf = self.cf(CF_RAW_OBJECTS)?;
|
|
let mode = IteratorMode::Start;
|
|
Ok(self.db.iterator_cf(cf, mode).filter_map(|res| res.ok()))
|
|
}
|
|
|
|
#[allow(dead_code)]
|
|
|
|
pub fn write_batch(&self, batch: WriteBatch) -> StorageResult<()> {
|
|
self.db
|
|
.write(batch)
|
|
.map_err(|e| StorageError::RocksDb(e.to_string()))?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
fn repository_view_key(rsync_uri: &str) -> String {
|
|
format!("{REPOSITORY_VIEW_KEY_PREFIX}{rsync_uri}")
|
|
}
|
|
|
|
fn repository_view_prefix(rsync_uri_prefix: &str) -> String {
|
|
format!("{REPOSITORY_VIEW_KEY_PREFIX}{rsync_uri_prefix}")
|
|
}
|
|
|
|
fn raw_by_hash_key(sha256_hex: &str) -> String {
|
|
format!("{RAW_BY_HASH_KEY_PREFIX}{sha256_hex}")
|
|
}
|
|
|
|
fn vcir_key(manifest_rsync_uri: &str) -> String {
|
|
format!("{VCIR_KEY_PREFIX}{manifest_rsync_uri}")
|
|
}
|
|
|
|
fn audit_rule_key(kind: AuditRuleKind, rule_hash: &str) -> String {
|
|
format!("{}{rule_hash}", kind.key_prefix())
|
|
}
|
|
|
|
fn rrdp_source_key(notify_uri: &str) -> String {
|
|
format!("{RRDP_SOURCE_KEY_PREFIX}{notify_uri}")
|
|
}
|
|
|
|
fn rrdp_source_member_key(notify_uri: &str, rsync_uri: &str) -> String {
|
|
format!("{RRDP_SOURCE_MEMBER_KEY_PREFIX}{notify_uri}:{rsync_uri}")
|
|
}
|
|
|
|
fn rrdp_source_member_prefix(notify_uri: &str) -> String {
|
|
format!("{RRDP_SOURCE_MEMBER_KEY_PREFIX}{notify_uri}:")
|
|
}
|
|
|
|
fn rrdp_uri_owner_key(rsync_uri: &str) -> String {
|
|
format!("{RRDP_URI_OWNER_KEY_PREFIX}{rsync_uri}")
|
|
}
|
|
|
|
fn encode_cbor<T: Serialize>(value: &T, entity: &'static str) -> StorageResult<Vec<u8>> {
|
|
serde_cbor::to_vec(value).map_err(|e| StorageError::Codec {
|
|
entity,
|
|
detail: e.to_string(),
|
|
})
|
|
}
|
|
|
|
fn decode_cbor<T: DeserializeOwned>(bytes: &[u8], entity: &'static str) -> StorageResult<T> {
|
|
serde_cbor::from_slice(bytes).map_err(|e| StorageError::Codec {
|
|
entity,
|
|
detail: e.to_string(),
|
|
})
|
|
}
|
|
|
|
fn validate_non_empty(field: &'static str, value: &str) -> StorageResult<()> {
|
|
if value.is_empty() {
|
|
return Err(StorageError::InvalidData {
|
|
entity: field,
|
|
detail: "must not be empty".to_string(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_sha256_hex(field: &'static str, value: &str) -> StorageResult<()> {
|
|
if value.len() != 64 || !value.as_bytes().iter().all(u8::is_ascii_hexdigit) {
|
|
return Err(StorageError::InvalidData {
|
|
entity: field,
|
|
detail: "must be a 64-character lowercase or uppercase SHA-256 hex string".to_string(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_manifest_number_be(field: &'static str, value: &[u8]) -> StorageResult<()> {
|
|
if value.is_empty() {
|
|
return Err(StorageError::InvalidData {
|
|
entity: field,
|
|
detail: "must not be empty".to_string(),
|
|
});
|
|
}
|
|
if value.len() > 20 {
|
|
return Err(StorageError::InvalidData {
|
|
entity: field,
|
|
detail: "must be at most 20 octets".to_string(),
|
|
});
|
|
}
|
|
if value.len() > 1 && value[0] == 0 {
|
|
return Err(StorageError::InvalidData {
|
|
entity: field,
|
|
detail: "must be minimal big-endian without leading zeros".to_string(),
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn parse_time(field: &'static str, value: &PackTime) -> StorageResult<time::OffsetDateTime> {
|
|
value.parse().map_err(|detail| StorageError::InvalidData {
|
|
entity: field,
|
|
detail,
|
|
})
|
|
}
|
|
|
|
fn enable_blobdb_if_supported(opts: &mut Options) {
|
|
#[allow(unused_mut)]
|
|
let mut _enabled = false;
|
|
|
|
#[allow(dead_code)]
|
|
fn _set(opts: &mut Options) {
|
|
opts.set_enable_blob_files(true);
|
|
opts.set_min_blob_size(1024);
|
|
}
|
|
|
|
_set(opts);
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct PackFile {
|
|
pub rsync_uri: String,
|
|
pub bytes: Vec<u8>,
|
|
pub sha256: [u8; 32],
|
|
}
|
|
|
|
impl PackFile {
|
|
pub fn new(rsync_uri: impl Into<String>, bytes: Vec<u8>, sha256: [u8; 32]) -> Self {
|
|
Self {
|
|
rsync_uri: rsync_uri.into(),
|
|
bytes,
|
|
sha256,
|
|
}
|
|
}
|
|
|
|
pub fn from_bytes_compute_sha256(rsync_uri: impl Into<String>, bytes: Vec<u8>) -> Self {
|
|
let sha256 = compute_sha256_32(&bytes);
|
|
Self::new(rsync_uri, bytes, sha256)
|
|
}
|
|
|
|
pub fn compute_sha256(&self) -> [u8; 32] {
|
|
compute_sha256_32(&self.bytes)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct PackTime {
|
|
pub rfc3339_utc: String,
|
|
}
|
|
|
|
impl PackTime {
|
|
pub fn from_utc_offset_datetime(t: time::OffsetDateTime) -> Self {
|
|
use time::format_description::well_known::Rfc3339;
|
|
let utc = t.to_offset(time::UtcOffset::UTC);
|
|
let s = utc.format(&Rfc3339).expect("format RFC 3339 UTC time");
|
|
Self { rfc3339_utc: s }
|
|
}
|
|
|
|
pub fn parse(&self) -> Result<time::OffsetDateTime, String> {
|
|
use time::format_description::well_known::Rfc3339;
|
|
time::OffsetDateTime::parse(&self.rfc3339_utc, &Rfc3339).map_err(|e| e.to_string())
|
|
}
|
|
}
|
|
|
|
fn compute_sha256_32(bytes: &[u8]) -> [u8; 32] {
|
|
let digest = sha2::Sha256::digest(bytes);
|
|
let mut out = [0u8; 32];
|
|
out.copy_from_slice(&digest);
|
|
out
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn pack_time(hour: i64) -> PackTime {
|
|
PackTime::from_utc_offset_datetime(
|
|
time::OffsetDateTime::UNIX_EPOCH + time::Duration::hours(hour),
|
|
)
|
|
}
|
|
|
|
fn sha256_hex(input: &[u8]) -> String {
|
|
hex::encode(compute_sha256_32(input))
|
|
}
|
|
|
|
fn sample_repository_view_entry(rsync_uri: &str, bytes: &[u8]) -> RepositoryViewEntry {
|
|
RepositoryViewEntry {
|
|
rsync_uri: rsync_uri.to_string(),
|
|
current_hash: Some(sha256_hex(bytes)),
|
|
repository_source: Some("https://rrdp.example.test/notification.xml".to_string()),
|
|
object_type: Some("cer".to_string()),
|
|
state: RepositoryViewState::Present,
|
|
}
|
|
}
|
|
|
|
fn sample_raw_by_hash_entry(bytes: Vec<u8>) -> RawByHashEntry {
|
|
RawByHashEntry {
|
|
sha256_hex: sha256_hex(&bytes),
|
|
bytes,
|
|
origin_uris: vec!["rsync://example.test/repo/object.cer".to_string()],
|
|
object_type: Some("cer".to_string()),
|
|
encoding: Some("der".to_string()),
|
|
}
|
|
}
|
|
|
|
fn sample_vcir(manifest_rsync_uri: &str) -> ValidatedCaInstanceResult {
|
|
let roa_bytes = b"roa-object".to_vec();
|
|
let ee_bytes = b"ee-cert".to_vec();
|
|
let child_bytes = b"child-cert".to_vec();
|
|
ValidatedCaInstanceResult {
|
|
manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
|
parent_manifest_rsync_uri: Some(
|
|
"rsync://example.test/repo/parent/parent.mft".to_string(),
|
|
),
|
|
tal_id: "apnic".to_string(),
|
|
ca_subject_name: "CN=Example CA".to_string(),
|
|
ca_ski: "00112233445566778899aabbccddeeff00112233".to_string(),
|
|
issuer_ski: "ffeeddccbbaa99887766554433221100ffeeddcc".to_string(),
|
|
last_successful_validation_time: pack_time(0),
|
|
current_manifest_rsync_uri: manifest_rsync_uri.to_string(),
|
|
current_crl_rsync_uri: "rsync://example.test/repo/current.crl".to_string(),
|
|
validated_manifest_meta: ValidatedManifestMeta {
|
|
validated_manifest_number: vec![3],
|
|
validated_manifest_this_update: pack_time(0),
|
|
validated_manifest_next_update: pack_time(24),
|
|
},
|
|
instance_gate: VcirInstanceGate {
|
|
manifest_next_update: pack_time(24),
|
|
current_crl_next_update: pack_time(12),
|
|
self_ca_not_after: pack_time(48),
|
|
instance_effective_until: pack_time(12),
|
|
},
|
|
child_entries: vec![VcirChildEntry {
|
|
child_manifest_rsync_uri: "rsync://example.test/repo/child/child.mft".to_string(),
|
|
child_cert_rsync_uri: "rsync://example.test/repo/child/child.cer".to_string(),
|
|
child_cert_hash: sha256_hex(&child_bytes),
|
|
child_ski: "1234567890abcdef1234567890abcdef12345678".to_string(),
|
|
child_rsync_base_uri: "rsync://example.test/repo/child/".to_string(),
|
|
child_publication_point_rsync_uri: "rsync://example.test/repo/child/".to_string(),
|
|
child_rrdp_notification_uri: Some(
|
|
"https://example.test/child-notify.xml".to_string(),
|
|
),
|
|
child_effective_ip_resources: None,
|
|
child_effective_as_resources: None,
|
|
accepted_at_validation_time: pack_time(0),
|
|
}],
|
|
local_outputs: vec![
|
|
VcirLocalOutput {
|
|
output_id: "vrp-1".to_string(),
|
|
output_type: VcirOutputType::Vrp,
|
|
item_effective_until: pack_time(12),
|
|
source_object_uri: "rsync://example.test/repo/object.roa".to_string(),
|
|
source_object_type: "roa".to_string(),
|
|
source_object_hash: sha256_hex(&roa_bytes),
|
|
source_ee_cert_hash: sha256_hex(&ee_bytes),
|
|
payload_json: r#"{"asn":64496,"prefix":"203.0.113.0/24"}"#.to_string(),
|
|
rule_hash: sha256_hex(b"vrp-rule-1"),
|
|
validation_path_hint: vec![manifest_rsync_uri.to_string()],
|
|
},
|
|
VcirLocalOutput {
|
|
output_id: "aspa-1".to_string(),
|
|
output_type: VcirOutputType::Aspa,
|
|
item_effective_until: pack_time(10),
|
|
source_object_uri: "rsync://example.test/repo/object.asa".to_string(),
|
|
source_object_type: "aspa".to_string(),
|
|
source_object_hash: sha256_hex(b"aspa-object"),
|
|
source_ee_cert_hash: sha256_hex(b"aspa-ee-cert"),
|
|
payload_json: r#"{"customer_as":64496,"providers":[64497]}"#.to_string(),
|
|
rule_hash: sha256_hex(b"aspa-rule-1"),
|
|
validation_path_hint: vec![manifest_rsync_uri.to_string()],
|
|
},
|
|
],
|
|
related_artifacts: vec![
|
|
VcirRelatedArtifact {
|
|
artifact_role: VcirArtifactRole::Manifest,
|
|
artifact_kind: VcirArtifactKind::Mft,
|
|
uri: Some(manifest_rsync_uri.to_string()),
|
|
sha256: sha256_hex(b"manifest-object"),
|
|
object_type: Some("mft".to_string()),
|
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
},
|
|
VcirRelatedArtifact {
|
|
artifact_role: VcirArtifactRole::CurrentCrl,
|
|
artifact_kind: VcirArtifactKind::Crl,
|
|
uri: Some("rsync://example.test/repo/current.crl".to_string()),
|
|
sha256: sha256_hex(b"current-crl"),
|
|
object_type: Some("crl".to_string()),
|
|
validation_status: VcirArtifactValidationStatus::Accepted,
|
|
},
|
|
],
|
|
summary: VcirSummary {
|
|
local_vrp_count: 1,
|
|
local_aspa_count: 1,
|
|
child_count: 1,
|
|
accepted_object_count: 4,
|
|
rejected_object_count: 0,
|
|
},
|
|
audit_summary: VcirAuditSummary {
|
|
failed_fetch_eligible: true,
|
|
last_failed_fetch_reason: None,
|
|
warning_count: 0,
|
|
audit_flags: vec!["validated-fresh".to_string()],
|
|
},
|
|
}
|
|
}
|
|
|
|
fn sample_audit_rule_entry(kind: AuditRuleKind) -> AuditRuleIndexEntry {
|
|
AuditRuleIndexEntry {
|
|
kind,
|
|
rule_hash: sha256_hex(match kind {
|
|
AuditRuleKind::Roa => b"roa-index-rule",
|
|
AuditRuleKind::Aspa => b"aspa-index-rule",
|
|
}),
|
|
manifest_rsync_uri: "rsync://example.test/repo/current.mft".to_string(),
|
|
source_object_uri: match kind {
|
|
AuditRuleKind::Roa => "rsync://example.test/repo/object.roa".to_string(),
|
|
AuditRuleKind::Aspa => "rsync://example.test/repo/object.asa".to_string(),
|
|
},
|
|
source_object_hash: sha256_hex(match kind {
|
|
AuditRuleKind::Roa => b"roa-object",
|
|
AuditRuleKind::Aspa => b"aspa-object",
|
|
}),
|
|
output_id: match kind {
|
|
AuditRuleKind::Roa => "vrp-1".to_string(),
|
|
AuditRuleKind::Aspa => "aspa-1".to_string(),
|
|
},
|
|
item_effective_until: pack_time(12),
|
|
}
|
|
}
|
|
|
|
fn sample_rrdp_source_record(notify_uri: &str) -> RrdpSourceRecord {
|
|
RrdpSourceRecord {
|
|
notify_uri: notify_uri.to_string(),
|
|
last_session_id: Some("session-1".to_string()),
|
|
last_serial: Some(42),
|
|
first_seen_at: pack_time(0),
|
|
last_seen_at: pack_time(1),
|
|
last_sync_at: Some(pack_time(1)),
|
|
sync_state: RrdpSourceSyncState::DeltaReady,
|
|
last_snapshot_uri: Some("https://rrdp.example.test/snapshot.xml".to_string()),
|
|
last_snapshot_hash: Some(sha256_hex(b"snapshot-bytes")),
|
|
last_error: None,
|
|
}
|
|
}
|
|
|
|
fn sample_rrdp_source_member_record(
|
|
notify_uri: &str,
|
|
rsync_uri: &str,
|
|
serial: u64,
|
|
) -> RrdpSourceMemberRecord {
|
|
RrdpSourceMemberRecord {
|
|
notify_uri: notify_uri.to_string(),
|
|
rsync_uri: rsync_uri.to_string(),
|
|
current_hash: Some(sha256_hex(rsync_uri.as_bytes())),
|
|
object_type: Some("cer".to_string()),
|
|
present: true,
|
|
last_confirmed_session_id: "session-1".to_string(),
|
|
last_confirmed_serial: serial,
|
|
last_changed_at: pack_time(serial as i64),
|
|
}
|
|
}
|
|
|
|
fn sample_rrdp_uri_owner_record(notify_uri: &str, rsync_uri: &str) -> RrdpUriOwnerRecord {
|
|
RrdpUriOwnerRecord {
|
|
rsync_uri: rsync_uri.to_string(),
|
|
notify_uri: notify_uri.to_string(),
|
|
current_hash: Some(sha256_hex(rsync_uri.as_bytes())),
|
|
last_confirmed_session_id: "session-1".to_string(),
|
|
last_confirmed_serial: 7,
|
|
last_changed_at: pack_time(7),
|
|
owner_state: RrdpUriOwnerState::Active,
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn rrdp_object_index_and_snapshot_delta_helpers_work_end_to_end() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
|
|
|
let notification_uri = "https://rrdp.example.test/notification.xml";
|
|
let u1 = "rsync://rpki.example.test/repo/obj1.cer".to_string();
|
|
let u2 = "rsync://rpki.example.test/repo/obj2.mft".to_string();
|
|
|
|
assert_eq!(
|
|
store
|
|
.rrdp_object_index_clear(notification_uri)
|
|
.expect("clear empty"),
|
|
0
|
|
);
|
|
|
|
let published_v1 = vec![(u1.clone(), vec![1u8, 2, 3]), (u2.clone(), vec![9u8, 8, 7])];
|
|
let n = store
|
|
.apply_rrdp_snapshot(notification_uri, &published_v1)
|
|
.expect("apply snapshot v1");
|
|
assert_eq!(n, 2);
|
|
|
|
assert_eq!(
|
|
store.get_raw(&u1).expect("get_raw u1"),
|
|
Some(vec![1u8, 2, 3])
|
|
);
|
|
assert_eq!(
|
|
store.get_raw(&u2).expect("get_raw u2"),
|
|
Some(vec![9u8, 8, 7])
|
|
);
|
|
assert!(
|
|
store
|
|
.rrdp_object_index_contains(notification_uri, &u1)
|
|
.expect("contains u1")
|
|
);
|
|
assert!(
|
|
store
|
|
.rrdp_object_index_contains(notification_uri, &u2)
|
|
.expect("contains u2")
|
|
);
|
|
|
|
let mut listed: Vec<String> = store
|
|
.rrdp_object_index_iter(notification_uri)
|
|
.expect("iter")
|
|
.collect();
|
|
listed.sort();
|
|
assert_eq!(listed, vec![u1.clone(), u2.clone()]);
|
|
|
|
let published_v2 = vec![(u2.clone(), vec![0u8, 1, 2, 3])];
|
|
store
|
|
.apply_rrdp_snapshot(notification_uri, &published_v2)
|
|
.expect("apply snapshot v2");
|
|
assert_eq!(store.get_raw(&u1).expect("get_raw removed"), None);
|
|
assert_eq!(
|
|
store.get_raw(&u2).expect("get_raw updated"),
|
|
Some(vec![0u8, 1, 2, 3])
|
|
);
|
|
|
|
let u3 = "rsync://rpki.example.test/repo/obj3.crl".to_string();
|
|
let ops = vec![
|
|
RrdpDeltaOp::Upsert {
|
|
rsync_uri: u3.clone(),
|
|
bytes: vec![4u8, 5, 6],
|
|
},
|
|
RrdpDeltaOp::Delete {
|
|
rsync_uri: u2.clone(),
|
|
},
|
|
];
|
|
let applied = store
|
|
.apply_rrdp_delta(notification_uri, &ops)
|
|
.expect("apply delta");
|
|
assert_eq!(applied, 2);
|
|
assert_eq!(store.get_raw(&u2).expect("get_raw deleted"), None);
|
|
assert_eq!(
|
|
store.get_raw(&u3).expect("get_raw u3"),
|
|
Some(vec![4u8, 5, 6])
|
|
);
|
|
|
|
let prefix = b"rsync://rpki.example.test/repo/";
|
|
let mut got: Vec<String> = store
|
|
.raw_iter_prefix(prefix)
|
|
.expect("raw_iter_prefix")
|
|
.map(|(k, _v)| String::from_utf8(k.to_vec()).expect("utf8 key"))
|
|
.collect();
|
|
got.sort();
|
|
assert_eq!(got, vec![u3.clone()]);
|
|
|
|
let all: Vec<String> = store
|
|
.raw_iter_all()
|
|
.expect("raw_iter_all")
|
|
.map(|(k, _v)| String::from_utf8(k.to_vec()).expect("utf8 key"))
|
|
.collect();
|
|
assert!(all.contains(&u3));
|
|
|
|
let cleared = store
|
|
.rrdp_object_index_clear(notification_uri)
|
|
.expect("clear");
|
|
assert!(cleared >= 1);
|
|
assert!(
|
|
!store
|
|
.rrdp_object_index_contains(notification_uri, &u3)
|
|
.expect("contains after clear")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn apply_rrdp_delta_empty_ops_is_noop() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
|
let n = store
|
|
.apply_rrdp_delta("https://rrdp.example.test/notification.xml", &[])
|
|
.expect("apply empty delta");
|
|
assert_eq!(n, 0);
|
|
}
|
|
|
|
#[test]
|
|
fn repository_view_and_raw_by_hash_roundtrip() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
|
|
|
let entry1 = sample_repository_view_entry("rsync://example.test/repo/a.cer", b"object-a");
|
|
let entry2 =
|
|
sample_repository_view_entry("rsync://example.test/repo/sub/b.roa", b"object-b");
|
|
store
|
|
.put_repository_view_entry(&entry1)
|
|
.expect("put repository view entry1");
|
|
store
|
|
.put_repository_view_entry(&entry2)
|
|
.expect("put repository view entry2");
|
|
|
|
let got1 = store
|
|
.get_repository_view_entry(&entry1.rsync_uri)
|
|
.expect("get repository view entry1")
|
|
.expect("entry1 exists");
|
|
assert_eq!(got1, entry1);
|
|
|
|
let got_prefix = store
|
|
.list_repository_view_entries_with_prefix("rsync://example.test/repo/sub/")
|
|
.expect("list repository view prefix");
|
|
assert_eq!(got_prefix, vec![entry2.clone()]);
|
|
|
|
store
|
|
.delete_repository_view_entry(&entry1.rsync_uri)
|
|
.expect("delete repository view entry1");
|
|
assert!(
|
|
store
|
|
.get_repository_view_entry(&entry1.rsync_uri)
|
|
.expect("get deleted repository view entry1")
|
|
.is_none()
|
|
);
|
|
|
|
let raw = sample_raw_by_hash_entry(b"raw-der-object".to_vec());
|
|
store
|
|
.put_raw_by_hash_entry(&raw)
|
|
.expect("put raw_by_hash entry");
|
|
let got_raw = store
|
|
.get_raw_by_hash_entry(&raw.sha256_hex)
|
|
.expect("get raw_by_hash entry")
|
|
.expect("raw entry exists");
|
|
assert_eq!(got_raw, raw);
|
|
}
|
|
|
|
#[test]
|
|
fn repository_view_and_raw_by_hash_validation_errors_are_reported() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
|
|
|
let invalid_view = RepositoryViewEntry {
|
|
rsync_uri: "rsync://example.test/repo/withdrawn.cer".to_string(),
|
|
current_hash: None,
|
|
repository_source: None,
|
|
object_type: None,
|
|
state: RepositoryViewState::Present,
|
|
};
|
|
let err = store
|
|
.put_repository_view_entry(&invalid_view)
|
|
.expect_err("missing current_hash must fail");
|
|
assert!(err.to_string().contains("current_hash is required"));
|
|
|
|
let invalid_raw = RawByHashEntry {
|
|
sha256_hex: sha256_hex(b"expected"),
|
|
bytes: b"actual".to_vec(),
|
|
origin_uris: vec!["rsync://example.test/repo/object.cer".to_string()],
|
|
object_type: None,
|
|
encoding: None,
|
|
};
|
|
let err = store
|
|
.put_raw_by_hash_entry(&invalid_raw)
|
|
.expect_err("mismatched raw_by_hash entry must fail");
|
|
assert!(err.to_string().contains("does not match bytes"));
|
|
}
|
|
|
|
#[test]
|
|
fn vcir_roundtrip_and_validation_failures_are_reported() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
|
|
|
let vcir = sample_vcir("rsync://example.test/repo/current.mft");
|
|
store.put_vcir(&vcir).expect("put vcir");
|
|
let got = store
|
|
.get_vcir(&vcir.manifest_rsync_uri)
|
|
.expect("get vcir")
|
|
.expect("vcir exists");
|
|
assert_eq!(got, vcir);
|
|
|
|
let mut invalid = sample_vcir("rsync://example.test/repo/invalid.mft");
|
|
invalid.summary.local_vrp_count = 9;
|
|
let err = store
|
|
.put_vcir(&invalid)
|
|
.expect_err("invalid vcir must fail");
|
|
assert!(err.to_string().contains("local_vrp_count=9"));
|
|
|
|
let mut invalid = sample_vcir("rsync://example.test/repo/invalid-2.mft");
|
|
invalid.instance_gate.instance_effective_until = pack_time(11);
|
|
let err = store
|
|
.put_vcir(&invalid)
|
|
.expect_err("invalid instance gate must fail");
|
|
assert!(err.to_string().contains("instance_effective_until"));
|
|
|
|
store
|
|
.delete_vcir(&vcir.manifest_rsync_uri)
|
|
.expect("delete vcir");
|
|
assert!(
|
|
store
|
|
.get_vcir(&vcir.manifest_rsync_uri)
|
|
.expect("get deleted vcir")
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn audit_rule_index_roundtrip_for_roa_and_aspa() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
|
|
|
let roa = sample_audit_rule_entry(AuditRuleKind::Roa);
|
|
let aspa = sample_audit_rule_entry(AuditRuleKind::Aspa);
|
|
store
|
|
.put_audit_rule_index_entry(&roa)
|
|
.expect("put roa audit rule entry");
|
|
store
|
|
.put_audit_rule_index_entry(&aspa)
|
|
.expect("put aspa audit rule entry");
|
|
|
|
let got_roa = store
|
|
.get_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
|
|
.expect("get roa audit rule entry")
|
|
.expect("roa entry exists");
|
|
let got_aspa = store
|
|
.get_audit_rule_index_entry(AuditRuleKind::Aspa, &aspa.rule_hash)
|
|
.expect("get aspa audit rule entry")
|
|
.expect("aspa entry exists");
|
|
assert_eq!(got_roa, roa);
|
|
assert_eq!(got_aspa, aspa);
|
|
|
|
store
|
|
.delete_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
|
|
.expect("delete roa audit rule entry");
|
|
assert!(
|
|
store
|
|
.get_audit_rule_index_entry(AuditRuleKind::Roa, &roa.rule_hash)
|
|
.expect("get deleted roa audit rule entry")
|
|
.is_none()
|
|
);
|
|
|
|
let mut invalid = sample_audit_rule_entry(AuditRuleKind::Roa);
|
|
invalid.rule_hash = "bad".to_string();
|
|
let err = store
|
|
.put_audit_rule_index_entry(&invalid)
|
|
.expect_err("invalid audit rule hash must fail");
|
|
assert!(err.to_string().contains("64-character"));
|
|
}
|
|
|
|
#[test]
|
|
fn storage_helpers_cover_optional_validation_paths() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
|
|
|
let withdrawn = RepositoryViewEntry {
|
|
rsync_uri: "rsync://example.test/repo/withdrawn.cer".to_string(),
|
|
current_hash: Some(sha256_hex(b"withdrawn")),
|
|
repository_source: Some("https://rrdp.example.test/notification.xml".to_string()),
|
|
object_type: Some("cer".to_string()),
|
|
state: RepositoryViewState::Withdrawn,
|
|
};
|
|
withdrawn
|
|
.validate_internal()
|
|
.expect("withdrawn repository view validates");
|
|
|
|
let raw = RawByHashEntry::from_bytes(sha256_hex(b"helper-bytes"), b"helper-bytes".to_vec());
|
|
raw.validate_internal()
|
|
.expect("raw_by_hash helper validates");
|
|
|
|
let empty_raw = RawByHashEntry {
|
|
sha256_hex: sha256_hex(b"x"),
|
|
bytes: Vec::new(),
|
|
origin_uris: Vec::new(),
|
|
object_type: None,
|
|
encoding: None,
|
|
};
|
|
let err = empty_raw
|
|
.validate_internal()
|
|
.expect_err("empty raw bytes must fail");
|
|
assert!(err.to_string().contains("bytes must not be empty"));
|
|
|
|
let duplicate_origin_raw = RawByHashEntry {
|
|
sha256_hex: sha256_hex(b"dup-origin"),
|
|
bytes: b"dup-origin".to_vec(),
|
|
origin_uris: vec![
|
|
"rsync://example.test/repo/object.cer".to_string(),
|
|
"rsync://example.test/repo/object.cer".to_string(),
|
|
],
|
|
object_type: Some("cer".to_string()),
|
|
encoding: Some("der".to_string()),
|
|
};
|
|
let err = duplicate_origin_raw
|
|
.validate_internal()
|
|
.expect_err("duplicate origin URI must fail");
|
|
assert!(err.to_string().contains("duplicate origin URI"));
|
|
|
|
store
|
|
.put_raw_batch(vec![(
|
|
"rsync://example.test/repo/raw.cer".to_string(),
|
|
vec![1, 2, 3],
|
|
)])
|
|
.expect("put_raw_batch stores entries");
|
|
assert_eq!(
|
|
store
|
|
.get_raw("rsync://example.test/repo/raw.cer")
|
|
.expect("get raw"),
|
|
Some(vec![1, 2, 3])
|
|
);
|
|
store
|
|
.delete_raw("rsync://example.test/repo/raw.cer")
|
|
.expect("delete raw entry");
|
|
assert!(
|
|
store
|
|
.get_raw("rsync://example.test/repo/raw.cer")
|
|
.expect("get deleted raw")
|
|
.is_none()
|
|
);
|
|
|
|
store
|
|
.put_rrdp_state("https://rrdp.example.test/notification.xml", b"state")
|
|
.expect("put rrdp state");
|
|
assert_eq!(
|
|
store
|
|
.get_rrdp_state("https://rrdp.example.test/notification.xml")
|
|
.expect("get rrdp state"),
|
|
Some(b"state".to_vec())
|
|
);
|
|
store
|
|
.delete_rrdp_state("https://rrdp.example.test/notification.xml")
|
|
.expect("delete rrdp state");
|
|
assert!(
|
|
store
|
|
.get_rrdp_state("https://rrdp.example.test/notification.xml")
|
|
.expect("get deleted rrdp state")
|
|
.is_none()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn rrdp_source_optional_fields_and_owner_without_hash_validate() {
|
|
let source = RrdpSourceRecord {
|
|
notify_uri: "https://rrdp.example.test/notification.xml".to_string(),
|
|
last_session_id: None,
|
|
last_serial: None,
|
|
first_seen_at: pack_time(0),
|
|
last_seen_at: pack_time(1),
|
|
last_sync_at: None,
|
|
sync_state: RrdpSourceSyncState::Empty,
|
|
last_snapshot_uri: None,
|
|
last_snapshot_hash: None,
|
|
last_error: Some("network timeout".to_string()),
|
|
};
|
|
source
|
|
.validate_internal()
|
|
.expect("source with optional fields validates");
|
|
|
|
let owner = RrdpUriOwnerRecord {
|
|
rsync_uri: "rsync://example.test/repo/object.cer".to_string(),
|
|
notify_uri: "https://rrdp.example.test/notification.xml".to_string(),
|
|
current_hash: None,
|
|
last_confirmed_session_id: "session-1".to_string(),
|
|
last_confirmed_serial: 5,
|
|
last_changed_at: pack_time(5),
|
|
owner_state: RrdpUriOwnerState::Withdrawn,
|
|
};
|
|
owner
|
|
.validate_internal()
|
|
.expect("owner without hash validates when withdrawn");
|
|
}
|
|
|
|
#[test]
|
|
fn rrdp_source_binding_records_roundtrip_and_prefix_iteration() {
|
|
let td = tempfile::tempdir().expect("tempdir");
|
|
let store = RocksStore::open(td.path()).expect("open rocksdb");
|
|
|
|
let notify_uri = "https://rrdp.example.test/notification.xml";
|
|
let source = sample_rrdp_source_record(notify_uri);
|
|
store
|
|
.put_rrdp_source_record(&source)
|
|
.expect("put rrdp source record");
|
|
let got_source = store
|
|
.get_rrdp_source_record(notify_uri)
|
|
.expect("get rrdp source record")
|
|
.expect("rrdp source exists");
|
|
assert_eq!(got_source, source);
|
|
|
|
let member1 =
|
|
sample_rrdp_source_member_record(notify_uri, "rsync://example.test/repo/a.cer", 1);
|
|
let member2 =
|
|
sample_rrdp_source_member_record(notify_uri, "rsync://example.test/repo/b.roa", 2);
|
|
let other_member = sample_rrdp_source_member_record(
|
|
"https://other.example.test/notification.xml",
|
|
"rsync://other.example.test/repo/c.cer",
|
|
3,
|
|
);
|
|
store
|
|
.put_rrdp_source_member_record(&member1)
|
|
.expect("put member1");
|
|
store
|
|
.put_rrdp_source_member_record(&member2)
|
|
.expect("put member2");
|
|
store
|
|
.put_rrdp_source_member_record(&other_member)
|
|
.expect("put other member");
|
|
|
|
let mut members = store
|
|
.list_rrdp_source_member_records(notify_uri)
|
|
.expect("list rrdp source members");
|
|
members.sort_by(|a, b| a.rsync_uri.cmp(&b.rsync_uri));
|
|
assert_eq!(members, vec![member1.clone(), member2.clone()]);
|
|
|
|
let got_member = store
|
|
.get_rrdp_source_member_record(notify_uri, &member1.rsync_uri)
|
|
.expect("get member1")
|
|
.expect("member1 exists");
|
|
assert_eq!(got_member, member1);
|
|
|
|
let owner = sample_rrdp_uri_owner_record(notify_uri, &member1.rsync_uri);
|
|
store
|
|
.put_rrdp_uri_owner_record(&owner)
|
|
.expect("put uri owner record");
|
|
let got_owner = store
|
|
.get_rrdp_uri_owner_record(&member1.rsync_uri)
|
|
.expect("get uri owner record")
|
|
.expect("uri owner exists");
|
|
assert_eq!(got_owner, owner);
|
|
store
|
|
.delete_rrdp_uri_owner_record(&member1.rsync_uri)
|
|
.expect("delete uri owner record");
|
|
assert!(
|
|
store
|
|
.get_rrdp_uri_owner_record(&member1.rsync_uri)
|
|
.expect("get deleted uri owner")
|
|
.is_none()
|
|
);
|
|
|
|
let mut invalid_source =
|
|
sample_rrdp_source_record("https://invalid.example/notification.xml");
|
|
invalid_source.last_snapshot_hash = Some("bad".to_string());
|
|
let err = store
|
|
.put_rrdp_source_record(&invalid_source)
|
|
.expect_err("invalid source hash must fail");
|
|
assert!(err.to_string().contains("last_snapshot_hash"));
|
|
|
|
let invalid_member = RrdpSourceMemberRecord {
|
|
notify_uri: notify_uri.to_string(),
|
|
rsync_uri: "rsync://example.test/repo/deleted.cer".to_string(),
|
|
current_hash: None,
|
|
object_type: None,
|
|
present: true,
|
|
last_confirmed_session_id: "session-1".to_string(),
|
|
last_confirmed_serial: 10,
|
|
last_changed_at: pack_time(10),
|
|
};
|
|
let err = store
|
|
.put_rrdp_source_member_record(&invalid_member)
|
|
.expect_err("present member without hash must fail");
|
|
assert!(err.to_string().contains("current_hash is required"));
|
|
}
|
|
}
|