phytrace_sdk/core/
provenance.rs

1//! Provenance signing for data integrity verification.
2//!
3//! Provides HMAC-SHA256 signing of UDM events to ensure data integrity
4//! and authenticity.
5
6use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
7use chrono::Utc;
8use hmac::{Hmac, Mac};
9use sha2::Sha256;
10
11use crate::core::config::ProvenanceConfig;
12use crate::error::{PhyTraceError, PhyTraceResult};
13use crate::models::event::{Provenance, UdmEvent};
14
15type HmacSha256 = Hmac<Sha256>;
16
17/// Provenance signer for UDM events.
18#[derive(Clone)]
19pub struct ProvenanceSigner {
20    config: ProvenanceConfig,
21    secret_key: Vec<u8>,
22}
23
24impl ProvenanceSigner {
25    /// Create a new signer from configuration.
26    pub fn from_config(config: &ProvenanceConfig) -> PhyTraceResult<Self> {
27        let secret_key = config.secret_key.as_ref().ok_or_else(|| {
28            PhyTraceError::Config(crate::error::ConfigError::MissingRequired(
29                "secret_key is required for signing".to_string(),
30            ))
31        })?;
32
33        // Decode base64-encoded key
34        let decoded_key = BASE64.decode(secret_key).map_err(|e| {
35            PhyTraceError::Config(crate::error::ConfigError::Validation(format!(
36                "Invalid base64 secret_key: {}",
37                e
38            )))
39        })?;
40
41        Ok(Self {
42            config: config.clone(),
43            secret_key: decoded_key,
44        })
45    }
46
47    /// Create a new signer with raw key bytes.
48    pub fn new(key_id: impl Into<String>, secret_key: Vec<u8>) -> Self {
49        Self {
50            config: ProvenanceConfig {
51                enabled: true,
52                algorithm: "hmac-sha256".to_string(),
53                key_id: Some(key_id.into()),
54                secret_key: None, // Not stored in config when using raw key
55                signed_fields: default_signed_fields(),
56            },
57            secret_key,
58        }
59    }
60
61    /// Sign an event and add provenance information.
62    pub fn sign(&self, event: &mut UdmEvent) -> PhyTraceResult<()> {
63        let signature_input = self.create_signature_input(event)?;
64        let signature = self.compute_signature(&signature_input)?;
65
66        event.provenance = Some(Provenance {
67            signature: Some(signature),
68            key_id: self.config.key_id.clone(),
69            algorithm: Some(self.config.algorithm.clone()),
70            signed_fields: Some(self.config.signed_fields.clone()),
71            signed_at: Some(Utc::now()),
72        });
73
74        Ok(())
75    }
76
77    /// Verify an event's provenance signature.
78    pub fn verify(&self, event: &UdmEvent) -> PhyTraceResult<bool> {
79        let provenance = event.provenance.as_ref().ok_or_else(|| {
80            PhyTraceError::Validation("Event has no provenance information".to_string())
81        })?;
82
83        let stored_signature = provenance
84            .signature
85            .as_ref()
86            .ok_or_else(|| PhyTraceError::Validation("Provenance has no signature".to_string()))?;
87
88        // Create a copy without provenance for verification
89        let mut event_copy = event.clone();
90        event_copy.provenance = None;
91
92        let signature_input = self.create_signature_input(&event_copy)?;
93        let computed_signature = self.compute_signature(&signature_input)?;
94
95        Ok(computed_signature == *stored_signature)
96    }
97
98    /// Create the canonical input for signing.
99    fn create_signature_input(&self, event: &UdmEvent) -> PhyTraceResult<String> {
100        // Build a canonical representation of signed fields
101        let mut parts = Vec::new();
102
103        for field in &self.config.signed_fields {
104            let value = match field.as_str() {
105                "event_id" => Some(event.event_id.clone()),
106                "captured_at" => Some(event.captured_at.to_rfc3339()),
107                "event_type" => Some(
108                    serde_json::to_string(&event.event_type)
109                        .unwrap_or_default()
110                        .trim_matches('"')
111                        .to_string(),
112                ),
113                "source_type" => Some(
114                    serde_json::to_string(&event.source_type)
115                        .unwrap_or_default()
116                        .trim_matches('"')
117                        .to_string(),
118                ),
119                "udm_version" => Some(event.udm_version.clone()),
120                "identity" => event
121                    .identity
122                    .as_ref()
123                    .and_then(|i| serde_json::to_string(i).ok()),
124                "location" => event
125                    .location
126                    .as_ref()
127                    .and_then(|l| serde_json::to_string(l).ok()),
128                _ => None, // Unknown fields are skipped
129            };
130
131            if let Some(v) = value {
132                parts.push(format!("{}={}", field, v));
133            }
134        }
135
136        Ok(parts.join("&"))
137    }
138
139    /// Compute HMAC-SHA256 signature.
140    fn compute_signature(&self, input: &str) -> PhyTraceResult<String> {
141        let mut mac = HmacSha256::new_from_slice(&self.secret_key)
142            .map_err(|e| PhyTraceError::Crypto(format!("Invalid key length: {}", e)))?;
143
144        mac.update(input.as_bytes());
145        let result = mac.finalize();
146        let signature = BASE64.encode(result.into_bytes());
147
148        Ok(signature)
149    }
150
151    /// Get the key ID.
152    pub fn key_id(&self) -> Option<&str> {
153        self.config.key_id.as_deref()
154    }
155
156    /// Get the algorithm.
157    pub fn algorithm(&self) -> &str {
158        &self.config.algorithm
159    }
160}
161
162fn default_signed_fields() -> Vec<String> {
163    vec![
164        "event_id".to_string(),
165        "timestamp".to_string(),
166        "source_type".to_string(),
167        "identity".to_string(),
168    ]
169}
170
171/// Generate a random secret key (32 bytes).
172pub fn generate_secret_key() -> String {
173    use rand::Rng;
174    let key: [u8; 32] = rand::thread_rng().gen();
175    BASE64.encode(key)
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::models::domains::IdentityDomain;
182    use crate::models::enums::SourceType;
183
184    fn test_signer() -> ProvenanceSigner {
185        // Use a fixed test key
186        let key = BASE64
187            .decode("dGVzdC1zZWNyZXQta2V5LWZvci1waHl0cmFjZS1zZGstdGVzdA==")
188            .unwrap();
189        ProvenanceSigner::new("test-key-001", key)
190    }
191
192    fn test_event() -> UdmEvent {
193        UdmEvent::new(SourceType::Amr).with_identity(IdentityDomain {
194            source_id: Some("robot-001".to_string()),
195            ..Default::default()
196        })
197    }
198
199    #[test]
200    fn test_sign_event() {
201        let signer = test_signer();
202        let mut event = test_event();
203
204        signer.sign(&mut event).unwrap();
205
206        assert!(event.provenance.is_some());
207        let prov = event.provenance.as_ref().unwrap();
208        assert!(prov.signature.is_some());
209        assert_eq!(prov.key_id, Some("test-key-001".to_string()));
210        assert_eq!(prov.algorithm, Some("hmac-sha256".to_string()));
211    }
212
213    #[test]
214    fn test_verify_valid_signature() {
215        let signer = test_signer();
216        let mut event = test_event();
217
218        signer.sign(&mut event).unwrap();
219        let is_valid = signer.verify(&event).unwrap();
220
221        assert!(is_valid);
222    }
223
224    #[test]
225    fn test_verify_tampered_event() {
226        let signer = test_signer();
227        let mut event = test_event();
228
229        signer.sign(&mut event).unwrap();
230
231        // Tamper with the event
232        event.event_id = "tampered-id".to_string();
233
234        let is_valid = signer.verify(&event).unwrap();
235        assert!(!is_valid);
236    }
237
238    #[test]
239    fn test_verify_missing_provenance() {
240        let signer = test_signer();
241        let event = test_event(); // Not signed
242
243        let result = signer.verify(&event);
244        assert!(result.is_err());
245    }
246
247    #[test]
248    fn test_different_keys_produce_different_signatures() {
249        let signer1 = ProvenanceSigner::new(
250            "key-1",
251            vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
252        );
253        let signer2 = ProvenanceSigner::new(
254            "key-2",
255            vec![16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1],
256        );
257
258        let mut event1 = test_event();
259        let mut event2 = event1.clone();
260
261        signer1.sign(&mut event1).unwrap();
262        signer2.sign(&mut event2).unwrap();
263
264        let sig1 = event1.provenance.unwrap().signature.unwrap();
265        let sig2 = event2.provenance.unwrap().signature.unwrap();
266
267        assert_ne!(sig1, sig2);
268    }
269
270    #[test]
271    fn test_from_config() {
272        let config = ProvenanceConfig {
273            enabled: true,
274            algorithm: "hmac-sha256".to_string(),
275            key_id: Some("config-key".to_string()),
276            secret_key: Some("dGVzdC1zZWNyZXQta2V5LWZvci1waHl0cmFjZS1zZGstdGVzdA==".to_string()),
277            signed_fields: vec!["event_id".to_string(), "timestamp".to_string()],
278        };
279
280        let signer = ProvenanceSigner::from_config(&config).unwrap();
281        assert_eq!(signer.key_id(), Some("config-key"));
282    }
283}