Skip to main content

cpop_protocol/rfc/
packet.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! RFC-compliant evidence packet structure for CBOR encoding.
4//!
5//! Implements the evidence-packet CDDL structure from draft-condrey-rats-pop-schema-01:
6//!
7//! ```cddl
8//! tagged-evidence-packet = #6.1129336656(evidence-packet)
9//!
10//! evidence-packet = {
11//!     1 => uint,                      ; version (1)
12//!     2 => vdf-structure,             ; VDF
13//!     3 => jitter-seal-structure,     ; Jitter Seal
14//!     4 => content-hash-tree,         ; Merkle for segments
15//!     5 => correlation-proof,         ; Spearman Correlation
16//!     6 => error-topology,            ; Fractal Error Pattern
17//!     7 => enclave-vise,              ; Hardware Observation Post
18//!     8 => zk-process-verdict,        ; Process Consistency Verdict
19//!     ? 9 => profile-declaration,     ; Profile tier and features
20//!     ? 18 => privacy-budget-certificate,
21//!     ? 19 => key-rotation-metadata,
22//!     * tstr => any,                  ; extensions
23//! }
24//! ```
25//!
26//! CBOR Semantic Tag: 1129336656 (0x43504F50, "CPOP" per IANA)
27
28use serde::{Deserialize, Serialize};
29use std::collections::HashMap;
30
31use super::fixed_point::{Centibits, Decibits, Millibits, RhoMillibits, SlopeDecibits};
32use super::serde_helpers::{hex_bytes_vec, hex_bytes_vec_opt};
33
34/// CBOR semantic tag for evidence packets.
35/// Per draft-condrey-rats-pop CDDL and IANA CBOR tag registry.
36pub const CBOR_TAG_EVIDENCE_PACKET: u64 = crate::codec::CBOR_TAG_CPOP;
37
38/// RFC-compliant evidence packet structure.
39///
40/// Uses integer keys (1-19) for compact CBOR encoding per the CDDL schema.
41/// The structure is designed for RATS (Remote ATtestation procedureS)
42/// compatibility while maintaining privacy-by-construction principles.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct PacketRfc {
45    /// Schema version (always 1 for this version).
46    /// Key 1 in CDDL.
47    #[serde(rename = "1")]
48    pub version: u32,
49
50    /// VDF structure proving minimum elapsed time.
51    /// Key 2 in CDDL.
52    #[serde(rename = "2")]
53    pub vdf: VdfStructure,
54
55    /// Jitter seal structure for behavioral entropy.
56    /// Key 3 in CDDL.
57    #[serde(rename = "3")]
58    pub jitter_seal: JitterSealStructure,
59
60    /// Content hash tree (Merkle structure).
61    /// Key 4 in CDDL.
62    #[serde(rename = "4")]
63    pub content_hash_tree: ContentHashTree,
64
65    /// Spearman correlation proof.
66    /// Key 5 in CDDL.
67    #[serde(rename = "5")]
68    pub correlation_proof: CorrelationProof,
69
70    /// Error topology analysis (fractal patterns).
71    /// Key 6 in CDDL.
72    #[serde(rename = "6", skip_serializing_if = "Option::is_none")]
73    pub error_topology: Option<ErrorTopology>,
74
75    /// Hardware observation post (enclave binding).
76    /// Key 7 in CDDL.
77    #[serde(rename = "7", skip_serializing_if = "Option::is_none")]
78    pub enclave_vise: Option<EnclaveVise>,
79
80    /// ZK process verdict for consistency.
81    /// Key 8 in CDDL.
82    #[serde(rename = "8", skip_serializing_if = "Option::is_none")]
83    pub zk_verdict: Option<ZkProcessVerdict>,
84
85    /// Profile declaration (tier and features).
86    /// Key 9 in CDDL.
87    #[serde(rename = "9", skip_serializing_if = "Option::is_none")]
88    pub profile: Option<ProfileDeclaration>,
89
90    /// Privacy budget certificate.
91    /// Key 18 in CDDL.
92    #[serde(rename = "18", skip_serializing_if = "Option::is_none")]
93    pub privacy_budget: Option<PrivacyBudgetCertificate>,
94
95    /// Key rotation metadata.
96    /// Key 19 in CDDL.
97    #[serde(rename = "19", skip_serializing_if = "Option::is_none")]
98    pub key_rotation: Option<KeyRotationMetadata>,
99
100    /// Vendor extensions (string keys).
101    #[serde(flatten, skip_serializing_if = "HashMap::is_empty")]
102    pub extensions: HashMap<String, serde_json::Value>,
103}
104
105/// VDF structure from CDDL.
106///
107/// ```cddl
108/// vdf-structure = {
109///     1 => bstr,           ; input: H(DST_CHAIN || content || jitter_seal)
110///     2 => bstr,           ; output
111///     3 => uint64,         ; iterations
112///     4 => [* uint64],     ; rdtsc_checkpoints (Continuous)
113///     5 => bstr,           ; entropic_pulse: HMAC(DST_CLOCK || SK, T ^ E)
114/// }
115/// ```
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct VdfStructure {
118    /// Input: H(DST_CHAIN || content || jitter_seal).
119    #[serde(rename = "1", with = "hex_bytes_vec")]
120    pub input: Vec<u8>,
121
122    /// VDF output.
123    #[serde(rename = "2", with = "hex_bytes_vec")]
124    pub output: Vec<u8>,
125
126    /// Number of iterations.
127    #[serde(rename = "3")]
128    pub iterations: u64,
129
130    /// RDTSC checkpoints for continuous verification.
131    #[serde(rename = "4")]
132    pub rdtsc_checkpoints: Vec<u64>,
133
134    /// Entropic pulse: HMAC(DST_CLOCK || SK, T ^ E).
135    #[serde(rename = "5", with = "hex_bytes_vec")]
136    pub entropic_pulse: Vec<u8>,
137}
138
139/// Jitter seal structure from CDDL.
140///
141/// ```cddl
142/// jitter-seal-structure = {
143///     1 => tstr,                      ; lang (e.g., "en-US")
144///     2 => bstr,                      ; bucket_commitment (ZK-Private)
145///     3 => uint,                      ; entropy_millibits
146///     4 => epsilon-centibits,         ; dp_epsilon_centibits
147///     5 => slope-decibits,            ; pink_noise_slope_decibits
148/// }
149/// ```
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct JitterSealStructure {
152    /// Language/locale identifier.
153    #[serde(rename = "1")]
154    pub lang: String,
155
156    /// Bucket commitment (ZK-Private).
157    #[serde(rename = "2", with = "hex_bytes_vec")]
158    pub bucket_commitment: Vec<u8>,
159
160    /// Entropy in millibits.
161    #[serde(rename = "3")]
162    pub entropy_millibits: u32,
163
164    /// Differential privacy epsilon (scaled x10000).
165    #[serde(rename = "4")]
166    pub dp_epsilon_centibits: Centibits,
167
168    /// Pink noise slope (scaled x10).
169    #[serde(rename = "5")]
170    pub pink_noise_slope_decibits: SlopeDecibits,
171}
172
173/// Content hash tree (Merkle structure).
174///
175/// ```cddl
176/// content-hash-tree = {
177///     1 => bstr,           ; root
178///     2 => uint16 .ge 20,  ; segment_count
179/// }
180/// ```
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct ContentHashTree {
183    /// Merkle root hash.
184    #[serde(rename = "1", with = "hex_bytes_vec")]
185    pub root: Vec<u8>,
186
187    /// Number of segments.
188    #[serde(rename = "2")]
189    pub segment_count: u16,
190}
191
192/// Correlation proof from CDDL.
193///
194/// ```cddl
195/// correlation-proof = {
196///     1 => int16 .within -1000..1000, ; rho (Scaled: -1000..1000)
197///     2 => 700,                        ; threshold (0.7 * 1000)
198/// }
199/// ```
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct CorrelationProof {
202    /// Spearman rho correlation coefficient (scaled x1000).
203    #[serde(rename = "1")]
204    pub rho: RhoMillibits,
205
206    /// Threshold for acceptance (default 700 = 0.7).
207    #[serde(rename = "2")]
208    pub threshold: i16,
209}
210
211impl Default for CorrelationProof {
212    fn default() -> Self {
213        Self {
214            rho: RhoMillibits::new(0),
215            threshold: 700,
216        }
217    }
218}
219
220/// Error topology structure.
221///
222/// Captures fractal error patterns for behavioral analysis.
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct ErrorTopology {
225    /// Fractal dimension of error patterns.
226    #[serde(rename = "1")]
227    pub fractal_dimension_decibits: Decibits,
228
229    /// Error clustering coefficient.
230    #[serde(rename = "2")]
231    pub clustering_millibits: Millibits,
232
233    /// Temporal distribution signature.
234    #[serde(rename = "3", with = "hex_bytes_vec")]
235    pub temporal_signature: Vec<u8>,
236}
237
238/// Hardware enclave binding.
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct EnclaveVise {
241    /// Enclave type (1=Secure Enclave, 16=TPM 2.0, 17=SGX).
242    #[serde(rename = "1")]
243    pub enclave_type: u8,
244
245    /// Attestation data from hardware.
246    #[serde(rename = "2", with = "hex_bytes_vec")]
247    pub attestation: Vec<u8>,
248
249    /// Timestamp of attestation.
250    #[serde(rename = "3")]
251    pub timestamp: u64,
252}
253
254/// ZK process verdict.
255#[derive(Debug, Clone, Serialize, Deserialize)]
256pub struct ZkProcessVerdict {
257    /// Verdict: 1=authentic, 2=suspicious, 3=inconclusive, 4=insufficient.
258    #[serde(rename = "1")]
259    pub verdict: u8,
260
261    /// Confidence in millibits.
262    #[serde(rename = "2")]
263    pub confidence_millibits: Millibits,
264
265    /// Proof data (optional, for STARK/SNARK).
266    #[serde(
267        rename = "3",
268        skip_serializing_if = "Option::is_none",
269        with = "hex_bytes_vec_opt"
270    )]
271    pub proof: Option<Vec<u8>>,
272}
273
274/// Profile declaration from CDDL.
275///
276/// ```cddl
277/// profile-declaration = {
278///     1 => profile-tier,              ; tier
279///     2 => profile-uri,               ; uri
280///     ? 3 => [+ feature-id],          ; enabled-features
281///     ? 4 => tstr,                    ; implementation-id
282/// }
283/// ```
284#[derive(Debug, Clone, Serialize, Deserialize)]
285pub struct ProfileDeclaration {
286    /// Profile tier: 1=core, 2=enhanced, 3=maximum.
287    #[serde(rename = "1")]
288    pub tier: u8,
289
290    /// Profile URN.
291    #[serde(rename = "2")]
292    pub uri: String,
293
294    /// Enabled features beyond MTI.
295    #[serde(rename = "3", skip_serializing_if = "Option::is_none")]
296    pub enabled_features: Option<Vec<u8>>,
297
298    /// Implementation identifier.
299    #[serde(rename = "4", skip_serializing_if = "Option::is_none")]
300    pub implementation_id: Option<String>,
301}
302
303impl ProfileDeclaration {
304    /// Create a Core tier profile.
305    pub fn core() -> Self {
306        Self {
307            tier: 1,
308            uri: "urn:ietf:params:pop:profile:1.0".to_string(),
309            enabled_features: None,
310            implementation_id: None,
311        }
312    }
313
314    /// Create an Enhanced tier profile.
315    pub fn enhanced() -> Self {
316        Self {
317            tier: 2,
318            uri: "urn:ietf:params:pop:profile:1.0".to_string(),
319            enabled_features: None,
320            implementation_id: None,
321        }
322    }
323
324    /// Create a Maximum tier profile.
325    pub fn maximum() -> Self {
326        Self {
327            tier: 3,
328            uri: "urn:ietf:params:pop:profile:1.0".to_string(),
329            enabled_features: None,
330            implementation_id: None,
331        }
332    }
333}
334
335/// Privacy budget certificate from CDDL.
336///
337/// Tracks differential privacy budget consumption per key period.
338#[derive(Debug, Clone, Serialize, Deserialize)]
339pub struct PrivacyBudgetCertificate {
340    /// Key generation method ("monthly", "weekly", "yearly").
341    #[serde(rename = "1")]
342    pub key_generation_method: String,
343
344    /// Key valid from (Unix timestamp).
345    #[serde(rename = "2")]
346    pub key_valid_from: u64,
347
348    /// Key valid until (Unix timestamp).
349    #[serde(rename = "3")]
350    pub key_valid_until: u64,
351
352    /// Session epsilon in centibits.
353    #[serde(rename = "4")]
354    pub session_epsilon_centibits: Centibits,
355
356    /// Cumulative epsilon before (fixed-point, 1e6 scale).
357    #[serde(rename = "5")]
358    pub cumulative_epsilon_micros_before: u64,
359
360    /// Cumulative epsilon after (fixed-point, 1e6 scale).
361    #[serde(rename = "6")]
362    pub cumulative_epsilon_micros_after: u64,
363
364    /// Sessions used with this key.
365    #[serde(rename = "7")]
366    pub sessions_used_this_key: u8,
367
368    /// Maximum sessions recommended.
369    #[serde(rename = "8")]
370    pub max_sessions_recommended: u8,
371}
372
373/// Key rotation metadata.
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct KeyRotationMetadata {
376    /// Rotation method ("monthly", "weekly", "yearly").
377    #[serde(rename = "1")]
378    pub rotation_method: String,
379
380    /// Next rotation date (Unix timestamp).
381    #[serde(rename = "2")]
382    pub next_rotation_date: u64,
383
384    /// Sessions remaining.
385    #[serde(rename = "3")]
386    pub sessions_remaining: u8,
387
388    /// Cumulative epsilon (fixed-point, 1e6 scale).
389    #[serde(rename = "4")]
390    pub cumulative_epsilon_micros: u64,
391}
392
393impl PacketRfc {
394    /// Create a minimal Core-tier packet.
395    pub fn new_core(
396        vdf: VdfStructure,
397        jitter_seal: JitterSealStructure,
398        content_hash_tree: ContentHashTree,
399        correlation_proof: CorrelationProof,
400    ) -> Self {
401        Self {
402            version: 1,
403            vdf,
404            jitter_seal,
405            content_hash_tree,
406            correlation_proof,
407            error_topology: None,
408            enclave_vise: None,
409            zk_verdict: None,
410            profile: Some(ProfileDeclaration::core()),
411            privacy_budget: None,
412            key_rotation: None,
413            extensions: HashMap::new(),
414        }
415    }
416
417    /// Validate the packet structure.
418    pub fn validate(&self) -> Vec<String> {
419        let mut errors = Vec::new();
420
421        if self.version != 1 {
422            errors.push(format!("unsupported version: {}", self.version));
423        }
424
425        if self.vdf.input.is_empty() {
426            errors.push("VDF input is empty".into());
427        }
428
429        if self.vdf.output.is_empty() {
430            errors.push("VDF output is empty".into());
431        }
432
433        if self.content_hash_tree.root.is_empty() {
434            errors.push("content hash tree root is empty".into());
435        }
436
437        if self.content_hash_tree.segment_count < 20 {
438            errors.push(format!(
439                "segment_count {} is below minimum 20",
440                self.content_hash_tree.segment_count
441            ));
442        }
443
444        if self.correlation_proof.threshold != 700 {
445            errors.push(format!(
446                "non-standard correlation threshold: {} (expected 700)",
447                self.correlation_proof.threshold
448            ));
449        }
450
451        errors
452    }
453
454    pub fn is_valid(&self) -> bool {
455        self.validate().is_empty()
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462
463    fn create_test_packet() -> PacketRfc {
464        PacketRfc::new_core(
465            VdfStructure {
466                input: vec![1u8; 32],
467                output: vec![2u8; 64],
468                iterations: 1_000_000,
469                rdtsc_checkpoints: vec![1000, 2000, 3000],
470                entropic_pulse: vec![3u8; 32],
471            },
472            JitterSealStructure {
473                lang: "en-US".to_string(),
474                bucket_commitment: vec![4u8; 32],
475                entropy_millibits: 8500,
476                dp_epsilon_centibits: Centibits::from_float(0.5),
477                pink_noise_slope_decibits: SlopeDecibits::from_float(-1.0),
478            },
479            ContentHashTree {
480                root: vec![5u8; 32],
481                segment_count: 25,
482            },
483            CorrelationProof {
484                rho: RhoMillibits::from_float(0.75),
485                threshold: 700,
486            },
487        )
488    }
489
490    #[test]
491    fn test_packet_creation() {
492        let packet = create_test_packet();
493        assert_eq!(packet.version, 1);
494        assert!(packet.profile.is_some());
495        assert_eq!(packet.profile.as_ref().unwrap().tier, 1);
496    }
497
498    #[test]
499    fn test_packet_validation() {
500        let packet = create_test_packet();
501        let errors = packet.validate();
502        assert!(errors.is_empty(), "errors: {:?}", errors);
503    }
504
505    #[test]
506    fn test_packet_validation_empty_vdf() {
507        let mut packet = create_test_packet();
508        packet.vdf.input = vec![];
509        let errors = packet.validate();
510        assert!(errors.iter().any(|e| e.contains("VDF input is empty")));
511    }
512
513    #[test]
514    fn test_packet_serialization() {
515        let packet = create_test_packet();
516        let json = serde_json::to_string(&packet).unwrap();
517
518        assert!(json.contains("\"1\":1"));
519        assert!(json.contains("\"2\":{"));
520        assert!(json.contains("\"3\":{"));
521
522        let decoded: PacketRfc = serde_json::from_str(&json).unwrap();
523        assert_eq!(decoded.version, packet.version);
524    }
525
526    #[test]
527    fn test_profile_tiers() {
528        assert_eq!(ProfileDeclaration::core().tier, 1);
529        assert_eq!(ProfileDeclaration::enhanced().tier, 2);
530        assert_eq!(ProfileDeclaration::maximum().tier, 3);
531    }
532}