use crate::security::verifier;
use bistun_core::{LmsError, RegistryStore, WormPayload};
use serde::Deserialize;
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
pub type PayloadFuture<'a> =
Pin<Box<dyn Future<Output = Result<(String, String), LmsError>> + Send + 'a>>;
#[derive(Debug, Deserialize)]
struct WormEnvelope {
metadata: HashMap<String, String>,
}
pub trait ISnapshotProvider: Send + Sync {
fn fetch_payload(&self) -> PayloadFuture<'_>;
}
#[cfg(feature = "simulation")]
pub struct SimulatedSnapshotProvider {
pub payload: String,
pub signature: String,
pub public_key: String,
}
#[cfg(feature = "simulation")]
impl Default for SimulatedSnapshotProvider {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "simulation")]
impl SimulatedSnapshotProvider {
#[must_use]
pub fn new() -> Self {
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use bistun_core::SIMULATED_WORM_JSON;
use ed25519_dalek::{Signer, SigningKey};
use rand::rngs::OsRng;
let mut csprng = OsRng;
let signing_key = SigningKey::generate(&mut csprng);
let public_key = BASE64.encode(signing_key.verifying_key().as_bytes());
let signature = BASE64.encode(signing_key.sign(SIMULATED_WORM_JSON.as_bytes()).to_bytes());
Self { payload: SIMULATED_WORM_JSON.to_string(), signature, public_key }
}
}
#[cfg(feature = "simulation")]
impl ISnapshotProvider for SimulatedSnapshotProvider {
fn fetch_payload(&self) -> PayloadFuture<'_> {
let p = self.payload.clone();
let s = self.signature.clone();
Box::pin(async move { Ok((p, s)) })
}
}
fn validate_pre_crypto_headers(json_payload: &str) -> Result<(), LmsError> {
let envelope: WormEnvelope =
serde_json::from_str(json_payload).map_err(|e| LmsError::SecurityFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "Header Validation".to_string(),
reason: format!("Payload lacks a valid metadata envelope: {e}"),
})?;
let schema = envelope.metadata.get("version").ok_or_else(|| LmsError::SecurityFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "Header Validation".to_string(),
reason: "Missing mandatory 'version' header".to_string(),
})?;
if !schema.starts_with("v1.")
&& !schema.starts_with("v2.")
&& schema != "1.0.0"
&& schema != "2.0.0"
{
return Err(LmsError::SecurityFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "Header Validation".to_string(),
reason: format!("Unsupported registry version: {schema}"),
});
}
if !envelope.metadata.contains_key("build_date") {
return Err(LmsError::SecurityFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "Header Validation".to_string(),
reason: "Missing mandatory 'build_date' header. Cannot prevent replay attacks."
.to_string(),
});
}
Ok(())
}
pub async fn hydrate_snapshot(
provider: &impl ISnapshotProvider,
public_key_b64: &str, ) -> Result<RegistryStore, LmsError> {
let (json_payload, signature) = provider.fetch_payload().await?;
validate_pre_crypto_headers(&json_payload)?;
verifier::verify_snapshot(&json_payload, &signature, public_key_b64)?;
let payload: WormPayload =
serde_json::from_str(&json_payload).map_err(|e| LmsError::SecurityFault {
pipeline_step: "Phase 0: WORM Hydration".to_string(),
context: "Deserialization".to_string(),
reason: format!("Failed to parse WORM JSON: {e}"),
})?;
let mut store = RegistryStore::new();
store.set_metadata(payload.metadata);
for profile in payload.profiles {
store.insert_stub(profile);
}
for (alias, canonical) in payload.aliases {
store.insert_alias(alias, canonical);
}
Ok(store)
}
#[cfg(all(test, feature = "simulation"))]
mod tests {
use super::*;
use mockall::mock;
mock! {
pub SnapshotProvider {}
impl ISnapshotProvider for SnapshotProvider {
fn fetch_payload<'a>(&'a self) -> PayloadFuture<'_>;
}
}
#[tokio::test]
async fn test_hydrate_snapshot_succeeds() {
let real_provider = SimulatedSnapshotProvider::new();
let p_clone = real_provider.payload.clone();
let s_clone = real_provider.signature.clone();
let mut mock_provider = MockSnapshotProvider::new();
mock_provider.expect_fetch_payload().returning(move || {
let p = p_clone.clone();
let s = s_clone.clone();
Box::pin(async move { Ok((p, s)) })
});
let store = hydrate_snapshot(&mock_provider, &real_provider.public_key)
.await
.expect("LMS-TEST: Hydration failed");
assert!(store.get_profile("en-US").is_some(), "System Default must exist");
assert!(store.get_profile("ar-EG").is_some());
assert!(store.get_profile("th-TH").is_some());
assert!(store.get_profile("zh-Hant").is_some());
assert_eq!(store.resolve_alias("in"), Some("id".to_string()));
assert_eq!(store.resolve_alias("zh-TW"), Some("zh-Hant".to_string()));
}
}