use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DpGuarantee {
pub mechanism: String,
pub epsilon: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta: Option<f64>,
pub composition_method: String,
pub total_queries: u32,
}
impl Default for DpGuarantee {
fn default() -> Self {
Self {
mechanism: "Laplace".to_string(),
epsilon: 1.0,
delta: None,
composition_method: "naive".to_string(),
total_queries: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct QualityMetrics {
#[serde(skip_serializing_if = "Option::is_none")]
pub benford_mad: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub correlation_preservation: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub statistical_fidelity: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mia_auc: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SyntheticDataCertificate {
pub certificate_id: String,
pub generation_timestamp: String,
pub generator_version: String,
pub config_hash: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub seed: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dp_guarantee: Option<DpGuarantee>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality_metrics: Option<QualityMetrics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub fingerprint_hash: Option<String>,
pub issuer: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
pub struct CertificateBuilder {
issuer: String,
dp_guarantee: Option<DpGuarantee>,
quality_metrics: Option<QualityMetrics>,
config_hash: Option<String>,
seed: Option<u64>,
fingerprint_hash: Option<String>,
generator_version: Option<String>,
}
impl CertificateBuilder {
pub fn new(issuer: impl Into<String>) -> Self {
Self {
issuer: issuer.into(),
dp_guarantee: None,
quality_metrics: None,
config_hash: None,
seed: None,
fingerprint_hash: None,
generator_version: None,
}
}
pub fn with_dp_guarantee(mut self, dp: DpGuarantee) -> Self {
self.dp_guarantee = Some(dp);
self
}
pub fn with_quality_metrics(mut self, metrics: QualityMetrics) -> Self {
self.quality_metrics = Some(metrics);
self
}
pub fn with_config_hash(mut self, hash: impl Into<String>) -> Self {
self.config_hash = Some(hash.into());
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn with_fingerprint_hash(mut self, hash: impl Into<String>) -> Self {
self.fingerprint_hash = Some(hash.into());
self
}
pub fn with_generator_version(mut self, version: impl Into<String>) -> Self {
self.generator_version = Some(version.into());
self
}
pub fn build(self) -> SyntheticDataCertificate {
SyntheticDataCertificate {
certificate_id: uuid::Uuid::new_v4().to_string(),
generation_timestamp: chrono::Utc::now().to_rfc3339(),
generator_version: self
.generator_version
.unwrap_or_else(|| env!("CARGO_PKG_VERSION").to_string()),
config_hash: self.config_hash.unwrap_or_default(),
seed: self.seed,
dp_guarantee: self.dp_guarantee,
quality_metrics: self.quality_metrics,
fingerprint_hash: self.fingerprint_hash,
issuer: self.issuer,
signature: None,
}
}
}
fn signable_content(certificate: &SyntheticDataCertificate) -> String {
let mut cert_copy = certificate.clone();
cert_copy.signature = None;
serde_json::to_string(&cert_copy).unwrap_or_default()
}
pub fn sign_certificate(certificate: &mut SyntheticDataCertificate, key_material: &str) {
let content = signable_content(certificate);
let signature = hmac_sha256(content.as_bytes(), key_material.as_bytes());
certificate.signature = Some(hex::encode(signature));
}
pub fn verify_certificate(certificate: &SyntheticDataCertificate, key_material: &str) -> bool {
let stored_sig = match &certificate.signature {
Some(s) => s.clone(),
None => return false,
};
let content = signable_content(certificate);
let expected = hmac_sha256(content.as_bytes(), key_material.as_bytes());
let expected_hex = hex::encode(expected);
if stored_sig.len() != expected_hex.len() {
return false;
}
let mut diff = 0u8;
for (a, b) in stored_sig.bytes().zip(expected_hex.bytes()) {
diff |= a ^ b;
}
diff == 0
}
fn hmac_sha256(data: &[u8], key: &[u8]) -> Vec<u8> {
let block_size = 64;
let mut key_padded = key.to_vec();
if key_padded.len() > block_size {
let mut hasher = Sha256::new();
hasher.update(&key_padded);
key_padded = hasher.finalize().to_vec();
}
key_padded.resize(block_size, 0);
let ipad: Vec<u8> = key_padded.iter().map(|b| b ^ 0x36).collect();
let opad: Vec<u8> = key_padded.iter().map(|b| b ^ 0x5c).collect();
let mut inner_hasher = Sha256::new();
inner_hasher.update(&ipad);
inner_hasher.update(data);
let inner_hash = inner_hasher.finalize();
let mut outer_hasher = Sha256::new();
outer_hasher.update(&opad);
outer_hasher.update(inner_hash);
outer_hasher.finalize().to_vec()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_creates_valid_certificate() {
let cert = CertificateBuilder::new("TestOrg")
.with_dp_guarantee(DpGuarantee {
mechanism: "Laplace".to_string(),
epsilon: 1.0,
delta: Some(1e-5),
composition_method: "naive".to_string(),
total_queries: 100,
})
.with_quality_metrics(QualityMetrics {
benford_mad: Some(0.01),
correlation_preservation: Some(0.95),
statistical_fidelity: Some(0.90),
mia_auc: Some(0.52),
})
.with_config_hash("deadbeef")
.with_seed(42)
.build();
assert!(!cert.certificate_id.is_empty());
assert!(!cert.generation_timestamp.is_empty());
assert_eq!(cert.issuer, "TestOrg");
assert_eq!(cert.config_hash, "deadbeef");
assert_eq!(cert.seed, Some(42));
assert!(cert.dp_guarantee.is_some());
assert!(cert.quality_metrics.is_some());
assert!(cert.signature.is_none());
let dp = cert.dp_guarantee.as_ref().expect("dp_guarantee present");
assert_eq!(dp.mechanism, "Laplace");
assert!((dp.epsilon - 1.0).abs() < 1e-10);
assert_eq!(dp.total_queries, 100);
}
#[test]
fn test_serde_roundtrip() {
let cert = CertificateBuilder::new("TestOrg")
.with_dp_guarantee(DpGuarantee::default())
.with_quality_metrics(QualityMetrics {
benford_mad: Some(0.015),
..QualityMetrics::default()
})
.with_seed(123)
.build();
let json = serde_json::to_string(&cert).expect("serialize");
let deserialized: SyntheticDataCertificate =
serde_json::from_str(&json).expect("deserialize");
assert_eq!(deserialized.certificate_id, cert.certificate_id);
assert_eq!(deserialized.issuer, cert.issuer);
assert_eq!(deserialized.seed, cert.seed);
assert_eq!(deserialized.dp_guarantee, cert.dp_guarantee);
}
#[test]
fn test_sign_and_verify_passes() {
let mut cert = CertificateBuilder::new("TestOrg")
.with_dp_guarantee(DpGuarantee {
mechanism: "Gaussian".to_string(),
epsilon: 0.5,
delta: Some(1e-6),
composition_method: "renyi_dp".to_string(),
total_queries: 200,
})
.with_config_hash("cafebabe")
.build();
sign_certificate(&mut cert, "super-secret-key-2024");
assert!(cert.signature.is_some());
assert!(verify_certificate(&cert, "super-secret-key-2024"));
}
#[test]
fn test_wrong_key_fails_verification() {
let mut cert = CertificateBuilder::new("TestOrg")
.with_dp_guarantee(DpGuarantee::default())
.build();
sign_certificate(&mut cert, "correct-key");
assert!(!verify_certificate(&cert, "wrong-key"));
}
#[test]
fn test_unsigned_certificate_fails_verification() {
let cert = CertificateBuilder::new("TestOrg")
.with_dp_guarantee(DpGuarantee::default())
.build();
assert!(!verify_certificate(&cert, "any-key"));
}
#[test]
fn test_tampered_certificate_fails_verification() {
let mut cert = CertificateBuilder::new("TestOrg")
.with_dp_guarantee(DpGuarantee {
mechanism: "Laplace".to_string(),
epsilon: 1.0,
delta: None,
composition_method: "naive".to_string(),
total_queries: 50,
})
.build();
sign_certificate(&mut cert, "secret");
assert!(verify_certificate(&cert, "secret"));
cert.issuer = "EvilOrg".to_string();
assert!(!verify_certificate(&cert, "secret"));
}
#[test]
fn test_builder_defaults() {
let cert = CertificateBuilder::new("MinimalOrg").build();
assert_eq!(cert.issuer, "MinimalOrg");
assert!(cert.dp_guarantee.is_none());
assert!(cert.quality_metrics.is_none());
assert!(cert.seed.is_none());
assert!(cert.fingerprint_hash.is_none());
assert!(cert.signature.is_none());
assert!(cert.config_hash.is_empty());
}
#[test]
fn test_fingerprint_hash_in_certificate() {
let cert = CertificateBuilder::new("Org")
.with_fingerprint_hash("sha256:abcdef0123456789")
.build();
assert_eq!(
cert.fingerprint_hash,
Some("sha256:abcdef0123456789".to_string())
);
}
}