Skip to main content

cpop_protocol/codec/
cbor.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! CBOR encoding/decoding for RFC 8949 compliance.
4//!
5//! Uses deterministic encoding (RFC 8949 Section 4.2) for reproducible serialization.
6
7use ciborium::value::Value;
8use serde::{de::DeserializeOwned, Serialize};
9use std::io::{Read, Write};
10
11use super::{CodecError, Result, CBOR_TAG_COMPACT_REF, CBOR_TAG_CPOP, CBOR_TAG_CWAR};
12
13/// Serialize a value to deterministic CBOR bytes.
14pub fn encode<T: Serialize>(value: &T) -> Result<Vec<u8>> {
15    let mut buffer = Vec::new();
16    ciborium::into_writer(value, &mut buffer).map_err(|e| CodecError::CborEncode(e.to_string()))?;
17    Ok(buffer)
18}
19
20/// Deserialize a value from CBOR bytes.
21pub fn decode<T: DeserializeOwned>(data: &[u8]) -> Result<T> {
22    ciborium::from_reader(data).map_err(|e| CodecError::CborDecode(e.to_string()))
23}
24
25/// Serialize a value as CBOR into a writer.
26pub fn encode_to<T: Serialize, W: Write>(value: &T, writer: W) -> Result<()> {
27    ciborium::into_writer(value, writer).map_err(|e| CodecError::CborEncode(e.to_string()))
28}
29
30/// Deserialize a value from a CBOR reader.
31pub fn decode_from<T: DeserializeOwned, R: Read>(reader: R) -> Result<T> {
32    ciborium::from_reader(reader).map_err(|e| CodecError::CborDecode(e.to_string()))
33}
34
35/// Encode with CPOP semantic tag (evidence packet).
36pub fn encode_cpop<T: Serialize>(value: &T) -> Result<Vec<u8>> {
37    encode_tagged(value, CBOR_TAG_CPOP)
38}
39
40/// Encode with CWAR semantic tag (attestation result).
41pub fn encode_cwar<T: Serialize>(value: &T) -> Result<Vec<u8>> {
42    encode_tagged(value, CBOR_TAG_CWAR)
43}
44
45/// Encode with compact evidence reference semantic tag.
46pub fn encode_compact_ref<T: Serialize>(value: &T) -> Result<Vec<u8>> {
47    encode_tagged(value, CBOR_TAG_COMPACT_REF)
48}
49
50/// Wrap a serialized value in a CBOR semantic tag.
51pub fn encode_tagged<T: Serialize>(value: &T, tag: u64) -> Result<Vec<u8>> {
52    let inner = encode(value)?;
53    let inner_value: Value =
54        ciborium::from_reader(&inner[..]).map_err(|e| CodecError::CborDecode(e.to_string()))?;
55
56    let tagged = Value::Tag(tag, Box::new(inner_value));
57
58    let mut buffer = Vec::new();
59    ciborium::into_writer(&tagged, &mut buffer)
60        .map_err(|e| CodecError::CborEncode(e.to_string()))?;
61
62    Ok(buffer)
63}
64
65/// Decode CBOR data, verifying the expected semantic tag.
66pub fn decode_tagged<T: DeserializeOwned>(data: &[u8], expected_tag: u64) -> Result<T> {
67    let value: Value =
68        ciborium::from_reader(data).map_err(|e| CodecError::CborDecode(e.to_string()))?;
69
70    match value {
71        Value::Tag(actual_tag, inner) => {
72            if actual_tag != expected_tag {
73                return Err(CodecError::InvalidTag {
74                    expected: expected_tag,
75                    actual: actual_tag,
76                });
77            }
78
79            let mut inner_bytes = Vec::new();
80            ciborium::into_writer(&*inner, &mut inner_bytes)
81                .map_err(|e| CodecError::CborEncode(e.to_string()))?;
82
83            ciborium::from_reader(&inner_bytes[..])
84                .map_err(|e| CodecError::CborDecode(e.to_string()))
85        }
86        _ => Err(CodecError::MissingTag),
87    }
88}
89
90/// Decode a CPOP-tagged evidence packet.
91pub fn decode_cpop<T: DeserializeOwned>(data: &[u8]) -> Result<T> {
92    decode_tagged(data, CBOR_TAG_CPOP)
93}
94
95/// Decode a CWAR-tagged attestation result.
96pub fn decode_cwar<T: DeserializeOwned>(data: &[u8]) -> Result<T> {
97    decode_tagged(data, CBOR_TAG_CWAR)
98}
99
100/// Decode a compact evidence reference.
101pub fn decode_compact_ref<T: DeserializeOwned>(data: &[u8]) -> Result<T> {
102    decode_tagged(data, CBOR_TAG_COMPACT_REF)
103}
104
105/// Check whether CBOR data carries the expected semantic tag.
106pub fn has_tag(data: &[u8], expected_tag: u64) -> bool {
107    if let Ok(value) = ciborium::from_reader::<Value, _>(data) {
108        matches!(value, Value::Tag(tag, _) if tag == expected_tag)
109    } else {
110        false
111    }
112}
113
114/// Extract the outermost CBOR semantic tag, if present.
115pub fn extract_tag(data: &[u8]) -> Option<u64> {
116    if let Ok(value) = ciborium::from_reader::<Value, _>(data) {
117        match value {
118            Value::Tag(tag, _) => Some(tag),
119            _ => None,
120        }
121    } else {
122        None
123    }
124}
125
126/// Integer keys per RFC CDDL definitions (smaller than string keys on the wire).
127pub mod keys {
128    pub const VERSION: i64 = 1;
129    pub const EXPORTED_AT: i64 = 2;
130    pub const STRENGTH: i64 = 3;
131    pub const PROVENANCE: i64 = 4;
132    pub const DOCUMENT: i64 = 5;
133    pub const CHECKPOINTS: i64 = 6;
134    pub const VDF_PARAMS: i64 = 7;
135    pub const CHAIN_HASH: i64 = 8;
136    pub const DECLARATION: i64 = 9;
137    pub const PRESENCE: i64 = 10;
138    pub const HARDWARE: i64 = 11;
139    pub const KEYSTROKE: i64 = 12;
140    pub const BEHAVIORAL: i64 = 13;
141    pub const CONTEXTS: i64 = 14;
142    pub const EXTERNAL: i64 = 15;
143    pub const KEY_HIERARCHY: i64 = 16;
144    pub const JITTER_BINDING: i64 = 17;
145    pub const TIME_EVIDENCE: i64 = 18;
146    pub const BIOLOGY_CLAIM: i64 = 19;
147    pub const CLAIMS: i64 = 20;
148
149    pub const ENTROPY_COMMITMENT: i64 = 21;
150    pub const SOURCES: i64 = 22;
151    pub const SUMMARY: i64 = 23;
152    pub const BINDING_MAC: i64 = 24;
153    pub const RAW_INTERVALS: i64 = 25;
154    pub const ACTIVE_PROBES: i64 = 26;
155    pub const LABYRINTH_STRUCTURE: i64 = 27;
156
157    pub const VALIDATION_STATUS: i64 = 31;
158    pub const MILLIBITS: i64 = 32;
159    pub const PARAMETER_VERSION: i64 = 33;
160    pub const THRESHOLDS: i64 = 34;
161
162    pub const BINDING_TIER: i64 = 41;
163    pub const TSA_RESPONSES: i64 = 42;
164    pub const BLOCKCHAIN_ANCHORS: i64 = 43;
165    pub const ROUGHTIME_SAMPLES: i64 = 44;
166
167    pub const INPUT: i64 = 51;
168    pub const OUTPUT: i64 = 52;
169    pub const ITERATIONS: i64 = 53;
170    pub const PROOF: i64 = 54;
171    pub const CALIBRATION: i64 = 55;
172
173    pub const GALTON_INVARIANT: i64 = 61;
174    pub const REFLEX_GATE: i64 = 62;
175
176    pub const EMBEDDING_DIMENSION: i64 = 71;
177    pub const TIME_DELAY: i64 = 72;
178    pub const ATTRACTOR_POINTS: i64 = 73;
179    pub const BETTI_NUMBERS: i64 = 74;
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185    use serde::{Deserialize, Serialize};
186
187    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
188    struct TestPacket {
189        version: i32,
190        data: Vec<u8>,
191    }
192
193    #[test]
194    fn test_tagged_roundtrip() {
195        let original = TestPacket {
196            version: 1,
197            data: vec![1, 2, 3, 4, 5],
198        };
199
200        let encoded = encode_cpop(&original).unwrap();
201        let decoded: TestPacket = decode_cpop(&encoded).unwrap();
202
203        assert_eq!(original, decoded);
204    }
205
206    #[test]
207    fn test_tag_detection() {
208        let packet = TestPacket {
209            version: 1,
210            data: vec![],
211        };
212
213        let cpop_encoded = encode_cpop(&packet).unwrap();
214        let cwar_encoded = encode_cwar(&packet).unwrap();
215
216        assert!(has_tag(&cpop_encoded, CBOR_TAG_CPOP));
217        assert!(!has_tag(&cpop_encoded, CBOR_TAG_CWAR));
218
219        assert!(has_tag(&cwar_encoded, CBOR_TAG_CWAR));
220        assert!(!has_tag(&cwar_encoded, CBOR_TAG_CPOP));
221    }
222
223    #[test]
224    fn test_tag_extraction() {
225        let packet = TestPacket {
226            version: 1,
227            data: vec![],
228        };
229
230        let encoded = encode_cpop(&packet).unwrap();
231        assert_eq!(extract_tag(&encoded), Some(CBOR_TAG_CPOP));
232
233        let untagged = encode(&packet).unwrap();
234        assert_eq!(extract_tag(&untagged), None);
235    }
236
237    #[test]
238    fn test_wrong_tag_error() {
239        let packet = TestPacket {
240            version: 1,
241            data: vec![],
242        };
243
244        let encoded = encode_cpop(&packet).unwrap();
245        let result: Result<TestPacket> = decode_cwar(&encoded);
246
247        assert!(matches!(
248            result,
249            Err(CodecError::InvalidTag {
250                expected: CBOR_TAG_CWAR,
251                actual: CBOR_TAG_CPOP
252            })
253        ));
254    }
255
256    #[test]
257    fn test_has_tag_on_untagged_data() {
258        let packet = TestPacket {
259            version: 1,
260            data: vec![10, 20],
261        };
262        let encoded = encode(&packet).unwrap();
263        assert!(!has_tag(&encoded, CBOR_TAG_CPOP));
264        assert!(!has_tag(&encoded, CBOR_TAG_CWAR));
265    }
266
267    #[test]
268    fn test_has_tag_on_invalid_cbor() {
269        // Garbage bytes that aren't valid CBOR
270        assert!(!has_tag(&[0xFF, 0xFE, 0xFD], CBOR_TAG_CPOP));
271        assert!(!has_tag(&[], CBOR_TAG_CPOP));
272    }
273
274    #[test]
275    fn test_extract_tag_on_invalid_cbor() {
276        assert_eq!(extract_tag(&[0xFF, 0xFE, 0xFD]), None);
277        assert_eq!(extract_tag(&[]), None);
278    }
279
280    #[test]
281    fn test_decode_tagged_on_untagged_data_returns_missing_tag() {
282        let packet = TestPacket {
283            version: 1,
284            data: vec![],
285        };
286        let encoded = encode(&packet).unwrap();
287        let result: Result<TestPacket> = decode_tagged(&encoded, CBOR_TAG_CPOP);
288        assert!(matches!(result, Err(CodecError::MissingTag)));
289    }
290
291    #[test]
292    fn test_decode_invalid_cbor_returns_error() {
293        let garbage = &[0xFF, 0xFE, 0xFD, 0xFC];
294        let result: Result<TestPacket> = decode(garbage);
295        assert!(matches!(result, Err(CodecError::CborDecode(_))));
296    }
297
298    #[test]
299    fn test_compact_ref_roundtrip() {
300        let packet = TestPacket {
301            version: 3,
302            data: vec![99, 100],
303        };
304
305        let encoded = encode_compact_ref(&packet).unwrap();
306        assert!(has_tag(&encoded, CBOR_TAG_COMPACT_REF));
307        assert_eq!(extract_tag(&encoded), Some(CBOR_TAG_COMPACT_REF));
308
309        let decoded: TestPacket = decode_compact_ref(&encoded).unwrap();
310        assert_eq!(packet, decoded);
311    }
312
313    #[test]
314    fn test_cwar_roundtrip() {
315        let packet = TestPacket {
316            version: 2,
317            data: vec![7, 8, 9],
318        };
319
320        let encoded = encode_cwar(&packet).unwrap();
321        assert!(has_tag(&encoded, CBOR_TAG_CWAR));
322
323        let decoded: TestPacket = decode_cwar(&encoded).unwrap();
324        assert_eq!(packet, decoded);
325    }
326
327    #[test]
328    fn test_encode_to_decode_from_cbor() {
329        let packet = TestPacket {
330            version: 5,
331            data: vec![1, 2, 3],
332        };
333
334        let mut buf = Vec::new();
335        encode_to(&packet, &mut buf).unwrap();
336        let decoded: TestPacket = decode_from(&buf[..]).unwrap();
337        assert_eq!(packet, decoded);
338    }
339}