1use 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#[derive(Clone)]
19pub struct ProvenanceSigner {
20 config: ProvenanceConfig,
21 secret_key: Vec<u8>,
22}
23
24impl ProvenanceSigner {
25 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 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 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, signed_fields: default_signed_fields(),
56 },
57 secret_key,
58 }
59 }
60
61 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 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 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 fn create_signature_input(&self, event: &UdmEvent) -> PhyTraceResult<String> {
100 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, };
130
131 if let Some(v) = value {
132 parts.push(format!("{}={}", field, v));
133 }
134 }
135
136 Ok(parts.join("&"))
137 }
138
139 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 pub fn key_id(&self) -> Option<&str> {
153 self.config.key_id.as_deref()
154 }
155
156 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
171pub 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 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 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(); 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}