pqc_binary_format/format.rs
1//! Core binary format implementation with serialization and validation.
2
3use crate::{
4 algorithm::Algorithm, error::CryptoError, metadata::PqcMetadata, Result, PQC_BINARY_VERSION,
5 PQC_MAGIC,
6};
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10/// Fixed header size in bytes: magic(4) + version(1) + algorithm_id(2) +
11/// flags(1) + metadata_len(4) + data_len(8) is split across the metadata
12/// section, so the bytes preceding the metadata section total 12.
13const HEADER_SIZE: usize = 12;
14
15/// SHA-256 checksum size in bytes.
16const CHECKSUM_SIZE: usize = 32;
17
18/// Branch-free, fixed-length equality for two 32-byte checksums.
19///
20/// Avoids leaking checksum-correctness information through timing.
21fn constant_time_eq(a: &[u8; 32], b: &[u8; 32]) -> bool {
22 let mut diff = 0u8;
23 for i in 0..32 {
24 diff |= a[i] ^ b[i];
25 }
26 diff == 0
27}
28
29/// PQC Binary Format v1.0 specification
30///
31/// This structure represents encrypted data in a standardized, self-describing format
32/// compatible across all post-quantum cryptographic algorithms.
33///
34/// # Example
35///
36/// ```
37/// use pqc_binary_format::{PqcBinaryFormat, Algorithm, PqcMetadata, EncParameters};
38/// use std::collections::HashMap;
39///
40/// let metadata = PqcMetadata {
41/// enc_params: EncParameters {
42/// iv: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
43/// tag: vec![1; 16],
44/// params: HashMap::new(),
45/// },
46/// ..Default::default()
47/// };
48///
49/// let format = PqcBinaryFormat::new(
50/// Algorithm::Hybrid,
51/// metadata,
52/// vec![1, 2, 3, 4, 5],
53///);
54///
55/// // Serialize to bytes
56/// let bytes = format.to_bytes().unwrap();
57///
58/// // Deserialize and verify
59/// let recovered = PqcBinaryFormat::from_bytes(&bytes).unwrap();
60/// assert_eq!(format, recovered);
61/// ```
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct PqcBinaryFormat {
64 /// Magic bytes - always "PQC\x01"
65 pub magic: [u8; 4],
66 /// Format version - currently 0x01
67 pub version: u8,
68 /// Algorithm identifier
69 pub algorithm: Algorithm,
70 /// Feature flags
71 pub flags: u8,
72 /// Algorithm-specific metadata
73 pub metadata: PqcMetadata,
74 /// Encrypted data payload
75 pub data: Vec<u8>,
76 /// SHA-256 checksum of the entire structure (excluding this field)
77 pub checksum: [u8; 32],
78}
79
80/// Feature flags for PQC Binary Format
81///
82/// Indicates optional features used in the encrypted data.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub struct FormatFlags(u8);
85
86impl FormatFlags {
87 /// Create new flags with all features disabled
88 #[must_use]
89 pub const fn new() -> Self {
90 Self(0)
91 }
92
93 /// Enable compression flag (bit 0)
94 #[must_use]
95 pub const fn with_compression(mut self) -> Self {
96 self.0 |= 0x01;
97 self
98 }
99
100 /// Enable streaming flag (bit 1)
101 #[must_use]
102 pub const fn with_streaming(mut self) -> Self {
103 self.0 |= 0x02;
104 self
105 }
106
107 /// Enable additional authentication flag (bit 2)
108 #[must_use]
109 pub const fn with_additional_auth(mut self) -> Self {
110 self.0 |= 0x04;
111 self
112 }
113
114 /// Enable experimental features flag (bit 3)
115 #[must_use]
116 pub const fn with_experimental(mut self) -> Self {
117 self.0 |= 0x08;
118 self
119 }
120
121 /// Check if compression is enabled
122 #[must_use]
123 pub const fn has_compression(self) -> bool {
124 (self.0 & 0x01) != 0
125 }
126
127 /// Check if streaming is enabled
128 #[must_use]
129 pub const fn has_streaming(self) -> bool {
130 (self.0 & 0x02) != 0
131 }
132
133 /// Check if additional authentication is enabled
134 #[must_use]
135 pub const fn has_additional_auth(self) -> bool {
136 (self.0 & 0x04) != 0
137 }
138
139 /// Check if experimental features are enabled
140 #[must_use]
141 pub const fn has_experimental(self) -> bool {
142 (self.0 & 0x08) != 0
143 }
144
145 /// Get raw flags value
146 #[must_use]
147 pub const fn as_u8(self) -> u8 {
148 self.0
149 }
150
151 /// Create flags from u8 value
152 #[must_use]
153 pub const fn from_u8(value: u8) -> Self {
154 Self(value)
155 }
156}
157
158impl Default for FormatFlags {
159 fn default() -> Self {
160 Self::new()
161 }
162}
163
164impl PqcBinaryFormat {
165 /// Create a new PQC binary format structure with default flags
166 ///
167 /// The checksum is automatically calculated and set.
168 ///
169 /// # Example
170 ///
171 /// ```
172 /// use pqc_binary_format::{PqcBinaryFormat, Algorithm, PqcMetadata, EncParameters};
173 /// use std::collections::HashMap;
174 ///
175 /// let metadata = PqcMetadata {
176 /// enc_params: EncParameters {
177 /// iv: vec![1; 12],
178 /// tag: vec![1; 16],
179 /// params: HashMap::new(),
180 /// },
181 /// ..Default::default()
182 /// };
183 ///
184 /// let format = PqcBinaryFormat::new(
185 /// Algorithm::PostQuantum,
186 /// metadata,
187 /// vec![1, 2, 3],
188 /// );
189 /// ```
190 #[must_use]
191 pub fn new(algorithm: Algorithm, metadata: PqcMetadata, data: Vec<u8>) -> Self {
192 let mut format = Self {
193 magic: PQC_MAGIC,
194 version: PQC_BINARY_VERSION,
195 algorithm,
196 flags: FormatFlags::new().as_u8(),
197 metadata,
198 data,
199 checksum: [0u8; 32],
200 };
201
202 // Calculate and set checksum
203 format.checksum = format.calculate_checksum();
204 format
205 }
206
207 /// Create with specific flags
208 ///
209 /// # Example
210 ///
211 /// ```
212 /// use pqc_binary_format::{PqcBinaryFormat, Algorithm, PqcMetadata, FormatFlags, EncParameters};
213 /// use std::collections::HashMap;
214 ///
215 /// let metadata = PqcMetadata {
216 /// enc_params: EncParameters {
217 /// iv: vec![1; 12],
218 /// tag: vec![1; 16],
219 /// params: HashMap::new(),
220 /// },
221 /// ..Default::default()
222 /// };
223 ///
224 /// let flags = FormatFlags::new().with_compression().with_streaming();
225 ///
226 /// let format = PqcBinaryFormat::with_flags(
227 /// Algorithm::Hybrid,
228 /// flags,
229 /// metadata,
230 /// vec![1, 2, 3],
231 /// );
232 ///
233 /// assert!(format.flags().has_compression());
234 /// assert!(format.flags().has_streaming());
235 /// ```
236 #[must_use]
237 pub fn with_flags(
238 algorithm: Algorithm,
239 flags: FormatFlags,
240 metadata: PqcMetadata,
241 data: Vec<u8>,
242 ) -> Self {
243 let mut format = Self {
244 magic: PQC_MAGIC,
245 version: PQC_BINARY_VERSION,
246 algorithm,
247 flags: flags.as_u8(),
248 metadata,
249 data,
250 checksum: [0u8; 32],
251 };
252
253 format.checksum = format.calculate_checksum();
254 format
255 }
256
257 /// Serialize to binary format
258 ///
259 /// # Errors
260 ///
261 /// Returns [`CryptoError::BinaryFormatError`] if:
262 /// - Format validation fails
263 /// - Binary serialization fails
264 ///
265 /// # Example
266 ///
267 /// ```
268 /// use pqc_binary_format::{PqcBinaryFormat, Algorithm, PqcMetadata, EncParameters};
269 /// use std::collections::HashMap;
270 ///
271 /// # let metadata = PqcMetadata {
272 /// # enc_params: EncParameters {
273 /// # iv: vec![1; 12],
274 /// # tag: vec![1; 16],
275 /// # params: HashMap::new(),
276 /// # },
277 /// # ..Default::default()
278 /// # };
279 /// # let format = PqcBinaryFormat::new(Algorithm::Hybrid, metadata, vec![1, 2, 3]);
280 /// let bytes = format.to_bytes().unwrap();
281 /// // Send bytes over network or save to file
282 /// ```
283 pub fn to_bytes(&self) -> Result<Vec<u8>> {
284 // Validate format before serialization
285 self.validate()?;
286
287 // Build the packed little-endian layout defined by the PQC Binary
288 // Format specification (draft-riddel-pqc-binary-format-00, Section 3):
289 // magic[4] | version[1] | algorithm_id[2 LE] | flags[1]
290 // | metadata_len[4 LE] | metadata(JSON) | data_len[8 LE] | data
291 // | checksum[32]
292 let mut buf = self.serialize_prefix();
293 let checksum: [u8; 32] = Sha256::digest(&buf).into();
294 buf.extend_from_slice(&checksum);
295 Ok(buf)
296 }
297
298 /// Serialize every field preceding the checksum, in spec order. The SHA-256
299 /// of this byte sequence is the integrity checksum.
300 ///
301 /// Lengths are written as the spec's fixed-width little-endian integers. A
302 /// metadata section larger than `u32::MAX` (4 GB) is saturated to `u32::MAX`;
303 /// such inputs are rejected by the practical size limits in any caller.
304 fn serialize_prefix(&self) -> Vec<u8> {
305 let metadata = self.metadata.to_json_bytes();
306 let mut buf = Vec::with_capacity(HEADER_SIZE + metadata.len() + self.data.len());
307 buf.extend_from_slice(&self.magic);
308 buf.push(self.version);
309 buf.extend_from_slice(&self.algorithm.as_id().to_le_bytes());
310 buf.push(self.flags);
311 let metadata_len = u32::try_from(metadata.len()).unwrap_or(u32::MAX);
312 buf.extend_from_slice(&metadata_len.to_le_bytes());
313 buf.extend_from_slice(&metadata);
314 buf.extend_from_slice(&(self.data.len() as u64).to_le_bytes());
315 buf.extend_from_slice(&self.data);
316 buf
317 }
318
319 /// Deserialize from binary format with checksum verification
320 ///
321 /// # Errors
322 ///
323 /// Returns [`CryptoError::BinaryFormatError`] if:
324 /// - Binary deserialization fails
325 /// - Format validation fails after deserialization
326 /// - Checksum verification fails
327 ///
328 /// # Example
329 ///
330 /// ```
331 /// use pqc_binary_format::PqcBinaryFormat;
332 ///
333 /// # use pqc_binary_format::{Algorithm, PqcMetadata, EncParameters};
334 /// # use std::collections::HashMap;
335 /// # let metadata = PqcMetadata {
336 /// # enc_params: EncParameters {
337 /// # iv: vec![1; 12],
338 /// # tag: vec![1; 16],
339 /// # params: HashMap::new(),
340 /// # },
341 /// # ..Default::default()
342 /// # };
343 /// # let format = PqcBinaryFormat::new(Algorithm::Hybrid, metadata, vec![1, 2, 3]);
344 /// # let bytes = format.to_bytes().unwrap();
345 /// let recovered = PqcBinaryFormat::from_bytes(&bytes).unwrap();
346 /// ```
347 pub fn from_bytes(data: &[u8]) -> Result<Self> {
348 // Smallest valid envelope: 12-byte pre-metadata header + 0-byte metadata
349 // + 8-byte data length + 0-byte data + 32-byte checksum = 52 bytes.
350 const MIN_SIZE: usize = HEADER_SIZE + 8 + CHECKSUM_SIZE;
351 if data.len() < MIN_SIZE {
352 return Err(CryptoError::BinaryFormatError(format!(
353 "Buffer too small: {} bytes, minimum {MIN_SIZE}",
354 data.len(),
355 )));
356 }
357
358 // Magic bytes
359 let magic: [u8; 4] = [data[0], data[1], data[2], data[3]];
360 if magic != PQC_MAGIC {
361 return Err(CryptoError::InvalidMagic);
362 }
363
364 // Version
365 let version = data[4];
366 if version != PQC_BINARY_VERSION {
367 return Err(CryptoError::UnsupportedVersion(version));
368 }
369
370 // Algorithm ID (little-endian) + flags
371 let algorithm_id = u16::from_le_bytes([data[5], data[6]]);
372 let algorithm = Algorithm::from_id(algorithm_id).ok_or_else(|| {
373 CryptoError::UnknownAlgorithm(format!("Invalid algorithm ID: {algorithm_id:#x}"))
374 })?;
375 let flags = data[7];
376
377 // Metadata length (little-endian) + metadata section
378 let metadata_len = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
379 let meta_start = HEADER_SIZE;
380 let meta_end = meta_start
381 .checked_add(metadata_len)
382 .ok_or_else(|| CryptoError::BinaryFormatError("Metadata length overflow".into()))?;
383 if meta_end + CHECKSUM_SIZE > data.len() {
384 return Err(CryptoError::BinaryFormatError(
385 "Metadata length exceeds buffer".into(),
386 ));
387 }
388 let metadata = PqcMetadata::from_json_bytes(&data[meta_start..meta_end])?;
389
390 // Data length (little-endian) + data section
391 let len_end = meta_end + 8;
392 if len_end + CHECKSUM_SIZE > data.len() {
393 return Err(CryptoError::BinaryFormatError(
394 "Truncated before data length".into(),
395 ));
396 }
397 let mut data_len_bytes = [0u8; 8];
398 data_len_bytes.copy_from_slice(&data[meta_end..len_end]);
399 let data_len = usize::try_from(u64::from_le_bytes(data_len_bytes))
400 .map_err(|_| CryptoError::BinaryFormatError("Data length exceeds usize".into()))?;
401 let data_end = len_end
402 .checked_add(data_len)
403 .ok_or_else(|| CryptoError::BinaryFormatError("Data length overflow".into()))?;
404 if data_end + CHECKSUM_SIZE != data.len() {
405 return Err(CryptoError::BinaryFormatError(format!(
406 "Length mismatch: expected {} bytes, got {}",
407 data_end + CHECKSUM_SIZE,
408 data.len()
409 )));
410 }
411 let payload = data[len_end..data_end].to_vec();
412
413 // Verify checksum over everything preceding it (constant-time compare).
414 // The exact-length check above guarantees 32 trailing bytes.
415 let mut stored = [0u8; 32];
416 stored.copy_from_slice(&data[data_end..]);
417 let calculated: [u8; 32] = Sha256::digest(&data[..data_end]).into();
418 if !constant_time_eq(&stored, &calculated) {
419 return Err(CryptoError::ChecksumMismatch);
420 }
421
422 let format = Self {
423 magic,
424 version,
425 algorithm,
426 flags,
427 metadata,
428 data: payload,
429 checksum: stored,
430 };
431 format.validate()?;
432 Ok(format)
433 }
434
435 /// Validate the binary format structure
436 ///
437 /// # Errors
438 ///
439 /// Returns [`CryptoError`] if:
440 /// - Magic bytes are invalid
441 /// - Version is unsupported
442 /// - Algorithm ID is invalid
443 /// - Metadata validation fails
444 pub fn validate(&self) -> Result<()> {
445 // Check magic bytes
446 if self.magic != PQC_MAGIC {
447 return Err(CryptoError::InvalidMagic);
448 }
449
450 // Check version
451 if self.version != PQC_BINARY_VERSION {
452 return Err(CryptoError::UnsupportedVersion(self.version));
453 }
454
455 // Validate algorithm
456 if Algorithm::from_id(self.algorithm.as_id()).is_none() {
457 return Err(CryptoError::UnknownAlgorithm(format!(
458 "Invalid algorithm ID: {:#x}",
459 self.algorithm.as_id()
460 )));
461 }
462
463 // Validate metadata
464 self.metadata.validate()?;
465
466 Ok(())
467 }
468
469 /// Update the checksum field with the calculated checksum
470 ///
471 /// Call this after modifying any fields to maintain integrity.
472 pub fn update_checksum(&mut self) {
473 self.checksum = self.calculate_checksum();
474 }
475
476 /// Calculate the SHA-256 integrity checksum over all fields preceding it.
477 ///
478 /// Equivalent to `SHA-256(serialize_prefix())`, where the metadata is the
479 /// canonical (sorted-key) JSON section — matching the deterministic checksum
480 /// definition in Section 3.3 of the specification.
481 fn calculate_checksum(&self) -> [u8; 32] {
482 Sha256::digest(self.serialize_prefix()).into()
483 }
484
485 /// Get format flags
486 #[must_use]
487 pub fn flags(&self) -> FormatFlags {
488 FormatFlags(self.flags)
489 }
490
491 /// Get algorithm
492 #[must_use]
493 pub const fn algorithm(&self) -> Algorithm {
494 self.algorithm
495 }
496
497 /// Get encrypted data
498 #[must_use]
499 pub fn data(&self) -> &[u8] {
500 &self.data
501 }
502
503 /// Get metadata
504 #[must_use]
505 pub const fn metadata(&self) -> &PqcMetadata {
506 &self.metadata
507 }
508
509 /// Get total size of the binary format when serialized
510 #[must_use]
511 pub fn total_size(&self) -> usize {
512 self.to_bytes().map_or(0, |bytes| bytes.len())
513 }
514}
515
516#[cfg(test)]
517mod tests {
518 use super::*;
519 use crate::EncParameters;
520 use std::collections::HashMap;
521
522 #[test]
523 fn test_format_flags() {
524 let flags = FormatFlags::new().with_compression().with_streaming();
525
526 assert!(flags.has_compression());
527 assert!(flags.has_streaming());
528 assert!(!flags.has_additional_auth());
529 assert!(!flags.has_experimental());
530 }
531
532 #[test]
533 fn test_binary_format_roundtrip() {
534 let metadata = PqcMetadata {
535 enc_params: EncParameters {
536 iv: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
537 tag: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
538 params: HashMap::new(),
539 },
540 ..Default::default()
541 };
542
543 let original = PqcBinaryFormat::new(Algorithm::Hybrid, metadata, vec![1, 2, 3, 4, 5]);
544
545 let bytes = original.to_bytes().unwrap();
546 let deserialized = PqcBinaryFormat::from_bytes(&bytes).unwrap();
547
548 assert_eq!(original, deserialized);
549 }
550
551 #[test]
552 fn test_checksum_validation() {
553 let metadata = PqcMetadata {
554 enc_params: EncParameters {
555 iv: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
556 tag: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
557 params: HashMap::new(),
558 },
559 ..Default::default()
560 };
561
562 let format = PqcBinaryFormat::new(Algorithm::PostQuantum, metadata, vec![1, 2, 3, 4, 5]);
563
564 let mut bytes = format.to_bytes().unwrap();
565
566 // Corrupt the data
567 if let Some(byte) = bytes.last_mut() {
568 *byte = byte.wrapping_add(1);
569 }
570
571 // Should fail checksum validation
572 assert!(PqcBinaryFormat::from_bytes(&bytes).is_err());
573 }
574
575 #[test]
576 fn test_flags_roundtrip() {
577 let metadata = PqcMetadata {
578 enc_params: EncParameters {
579 iv: vec![1; 12],
580 tag: vec![1; 16],
581 params: HashMap::new(),
582 },
583 ..Default::default()
584 };
585
586 let flags = FormatFlags::new()
587 .with_compression()
588 .with_streaming()
589 .with_additional_auth();
590
591 let format =
592 PqcBinaryFormat::with_flags(Algorithm::QuadLayer, flags, metadata, vec![1, 2, 3]);
593
594 let bytes = format.to_bytes().unwrap();
595 let recovered = PqcBinaryFormat::from_bytes(&bytes).unwrap();
596
597 assert!(recovered.flags().has_compression());
598 assert!(recovered.flags().has_streaming());
599 assert!(recovered.flags().has_additional_auth());
600 assert!(!recovered.flags().has_experimental());
601 }
602}