Skip to main content

vwh_core/
format.rs

1use crate::{ArtifactId, Error, Intent, KeyFingerprint, Result};
2use chrono::{DateTime, TimeZone, Utc};
3use std::io::{Read, Write};
4
5pub const MAGIC: &[u8; 4] = b"VWH\0";
6pub const VERSION_V1: u16 = 1;
7pub const VERSION_V2: u16 = 2;
8pub const ARTIFACT_SIZE_V1: usize = 128;
9pub const ARTIFACT_SIZE_V2: usize = 256;
10
11// V1 FLAGS byte bit definitions
12pub const FLAG_SEALED: u8 = 0b00000001;
13
14// Zero markers
15pub const ZERO_PUBKEY: [u8; 32] = [0u8; 32];
16pub const ZERO_HASH: [u8; 32] = [0u8; 32];
17pub const ZERO_SIGNATURE: [u8; 64] = [0u8; 64];
18
19/// Artifact version
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ArtifactVersion {
22    V1,
23    V2,
24}
25
26impl std::fmt::Display for ArtifactVersion {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        match self {
29            ArtifactVersion::V1 => write!(f, "v1"),
30            ArtifactVersion::V2 => write!(f, "v2"),
31        }
32    }
33}
34
35impl ArtifactVersion {
36    pub fn as_u16(&self) -> u16 {
37        match self {
38            ArtifactVersion::V1 => VERSION_V1,
39            ArtifactVersion::V2 => VERSION_V2,
40        }
41    }
42    
43    pub fn from_u16(v: u16) -> Result<Self> {
44        match v {
45            VERSION_V1 => Ok(ArtifactVersion::V1),
46            VERSION_V2 => Ok(ArtifactVersion::V2),
47            _ => Err(Error::UnsupportedVersion(v)),
48        }
49    }
50}
51
52/// Artifact state
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum ArtifactState {
55    /// V1: Unsigned draft | V2: No author signature
56    Draft,
57    /// V1: Signed but not sealed | V2: Author signed but not sealed
58    Signed,
59    /// V1: Sealed (FLAGS bit set) | V2: Dual-signed (author + seal)
60    Sealed,
61}
62
63/// Complete artifact structure (supports both v1 and v2)
64#[derive(Debug, Clone)]
65pub struct Artifact {
66    pub version: ArtifactVersion,
67    
68    // Common fields (present in both v1 and v2)
69    pub artifact_id: ArtifactId,
70    pub timestamp: DateTime<Utc>,
71    pub intent: Intent,
72    pub author_pubkey: [u8; 32],
73    pub author_signature: [u8; 64],
74    
75    // V1-specific fields
76    pub flags: u8,  // Only used in v1
77    
78    // V2-specific fields
79    pub reserved_a: u8,
80    pub note_hash: [u8; 32],
81    pub seal_pubkey: [u8; 32],
82    pub seal_signature: [u8; 64],
83}
84
85impl Artifact {
86    /// Parse artifact from bytes (auto-detects v1 vs v2)
87    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
88        // Check minimum size for magic + version
89        if bytes.len() < 6 {
90            return Err(Error::FileTooSmall { expected: 6, actual: bytes.len() });
91        }
92        
93        // Check magic
94        if &bytes[0..4] != MAGIC {
95            return Err(Error::InvalidMagic);
96        }
97        
98        // Read version
99        let version_u16 = u16::from_le_bytes([bytes[4], bytes[5]]);
100        let version = ArtifactVersion::from_u16(version_u16)?;
101        
102        match version {
103            ArtifactVersion::V1 => Self::from_bytes_v1(bytes),
104            ArtifactVersion::V2 => Self::from_bytes_v2(bytes),
105        }
106    }
107    
108    /// Parse v1 artifact (128 bytes)
109    fn from_bytes_v1(bytes: &[u8]) -> Result<Self> {
110        if bytes.len() != ARTIFACT_SIZE_V1 {
111            return Err(Error::FileTooSmall {
112                expected: ARTIFACT_SIZE_V1,
113                actual: bytes.len(),
114            });
115        }
116
117        let mut cursor = 0;
118
119        // Magic (4 bytes) - already validated
120        cursor += 4;
121
122        // Version (2 bytes) - already validated
123        cursor += 2;
124
125        // Flags (1 byte)
126        let flags = bytes[cursor];
127        cursor += 1;
128
129        // Artifact ID (16 bytes)
130        let mut artifact_id_bytes = [0u8; 16];
131        artifact_id_bytes.copy_from_slice(&bytes[cursor..cursor + 16]);
132        let artifact_id = ArtifactId::from_bytes(artifact_id_bytes);
133        cursor += 16;
134
135        // Timestamp (8 bytes, little-endian)
136        let timestamp_secs =
137            u64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap());
138        let timestamp = Utc
139            .timestamp_opt(timestamp_secs as i64, 0)
140            .single()
141            .ok_or_else(|| Error::UnexpectedEof {
142                field: "timestamp".to_string(),
143            })?;
144        cursor += 8;
145
146        // Intent (1 byte)
147        let intent = Intent::from_u8(bytes[cursor])?;
148        cursor += 1;
149
150        // Author public key (32 bytes)
151        let mut author_pubkey = [0u8; 32];
152        author_pubkey.copy_from_slice(&bytes[cursor..cursor + 32]);
153        cursor += 32;
154
155        // Signature (64 bytes)
156        let mut author_signature = [0u8; 64];
157        author_signature.copy_from_slice(&bytes[cursor..cursor + 64]);
158
159        Ok(Artifact {
160            version: ArtifactVersion::V1,
161            artifact_id,
162            timestamp,
163            intent,
164            author_pubkey,
165            author_signature,
166            flags,
167            reserved_a: 0,
168            note_hash: ZERO_HASH,
169            seal_pubkey: ZERO_PUBKEY,
170            seal_signature: ZERO_SIGNATURE,
171        })
172    }
173    
174    /// Parse v2 artifact (256 bytes)
175    fn from_bytes_v2(bytes: &[u8]) -> Result<Self> {
176        if bytes.len() != ARTIFACT_SIZE_V2 {
177            return Err(Error::FileTooSmall {
178                expected: ARTIFACT_SIZE_V2,
179                actual: bytes.len(),
180            });
181        }
182
183        let mut cursor = 0;
184
185        // Magic (4 bytes) - already validated
186        cursor += 4;
187
188        // Version (2 bytes) - already validated
189        cursor += 2;
190
191        // RESERVED_A (1 byte)
192        let reserved_a = bytes[cursor];
193        cursor += 1;
194
195        // Artifact ID (16 bytes)
196        let mut artifact_id_bytes = [0u8; 16];
197        artifact_id_bytes.copy_from_slice(&bytes[cursor..cursor + 16]);
198        let artifact_id = ArtifactId::from_bytes(artifact_id_bytes);
199        cursor += 16;
200
201        // Timestamp (8 bytes, little-endian)
202        let timestamp_secs =
203            u64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap());
204        let timestamp = Utc
205            .timestamp_opt(timestamp_secs as i64, 0)
206            .single()
207            .ok_or_else(|| Error::UnexpectedEof {
208                field: "timestamp".to_string(),
209            })?;
210        cursor += 8;
211
212        // Intent (1 byte)
213        let intent = Intent::from_u8(bytes[cursor])?;
214        cursor += 1;
215
216        // Author public key (32 bytes)
217        let mut author_pubkey = [0u8; 32];
218        author_pubkey.copy_from_slice(&bytes[cursor..cursor + 32]);
219        cursor += 32;
220
221        // NOTE_HASH (32 bytes)
222        let mut note_hash = [0u8; 32];
223        note_hash.copy_from_slice(&bytes[cursor..cursor + 32]);
224        cursor += 32;
225
226        // AUTHOR_SIGNATURE (64 bytes)
227        let mut author_signature = [0u8; 64];
228        author_signature.copy_from_slice(&bytes[cursor..cursor + 64]);
229        cursor += 64;
230
231        // SEAL_PUBKEY (32 bytes)
232        let mut seal_pubkey = [0u8; 32];
233        seal_pubkey.copy_from_slice(&bytes[cursor..cursor + 32]);
234        cursor += 32;
235
236        // SEAL_SIGNATURE (64 bytes)
237        let mut seal_signature = [0u8; 64];
238        seal_signature.copy_from_slice(&bytes[cursor..cursor + 64]);
239
240        Ok(Artifact {
241            version: ArtifactVersion::V2,
242            artifact_id,
243            timestamp,
244            intent,
245            author_pubkey,
246            author_signature,
247            flags: 0,
248            reserved_a,
249            note_hash,
250            seal_pubkey,
251            seal_signature,
252        })
253    }
254
255    /// Get bytes that were signed by author
256    pub fn author_signing_bytes(&self) -> Vec<u8> {
257        match self.version {
258            ArtifactVersion::V1 => {
259                // V1: 63 bytes (FLAGS excluded)
260                let mut bytes = Vec::with_capacity(63);
261                bytes.extend_from_slice(MAGIC);
262                bytes.extend_from_slice(&VERSION_V1.to_le_bytes());
263                // FLAGS NOT INCLUDED (it's state, not identity)
264                bytes.extend_from_slice(&self.artifact_id.0);
265                bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());
266                bytes.push(self.intent.to_u8());
267                bytes.extend_from_slice(&self.author_pubkey);
268                bytes
269            },
270            ArtifactVersion::V2 => {
271                // V2: 103 bytes (everything before author_signature)
272                let mut bytes = Vec::with_capacity(103);
273                bytes.extend_from_slice(MAGIC);  // 4
274                bytes.extend_from_slice(&VERSION_V2.to_le_bytes());  // 2
275                bytes.push(self.reserved_a);  // 1
276                bytes.extend_from_slice(&self.artifact_id.0);  // 16
277                bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());  // 8
278                bytes.push(self.intent.to_u8());  // 1
279                bytes.extend_from_slice(&self.author_pubkey);  // 32
280                bytes.extend_from_slice(&self.note_hash);  // 32
281                bytes
282            },
283        }
284    }
285    
286    /// Get bytes that were signed by seal (V2 only).
287    /// Returns Err if the artifact has no seal (seal_pubkey is all zeros).
288    pub fn seal_signing_bytes(&self) -> Result<Vec<u8>> {
289        // All-zeros pubkey means no seal has been applied
290        if self.seal_pubkey == [0u8; 32] {
291            return Err(Error::NoSeal);
292        }
293
294        // 192 bytes: everything except seal_signature
295        let mut bytes = Vec::with_capacity(192);
296        bytes.extend_from_slice(MAGIC);  // 4
297        bytes.extend_from_slice(&VERSION_V2.to_le_bytes());  // 2
298        bytes.push(self.reserved_a);  // 1
299        bytes.extend_from_slice(&self.artifact_id.0);  // 16
300        bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());  // 8
301        bytes.push(self.intent.to_u8());  // 1
302        bytes.extend_from_slice(&self.author_pubkey);  // 32
303        bytes.extend_from_slice(&self.note_hash);  // 32
304        bytes.extend_from_slice(&self.author_signature);  // 64
305        bytes.extend_from_slice(&self.seal_pubkey);  // 32
306        Ok(bytes)
307    }
308
309    /// Serialize artifact to bytes
310    pub fn to_bytes(&self) -> Vec<u8> {
311        match self.version {
312            ArtifactVersion::V1 => self.to_bytes_v1(),
313            ArtifactVersion::V2 => self.to_bytes_v2(),
314        }
315    }
316    
317    fn to_bytes_v1(&self) -> Vec<u8> {
318        let mut bytes = Vec::with_capacity(ARTIFACT_SIZE_V1);
319        
320        bytes.extend_from_slice(MAGIC);
321        bytes.extend_from_slice(&VERSION_V1.to_le_bytes());
322        bytes.push(self.flags);
323        bytes.extend_from_slice(&self.artifact_id.0);
324        bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());
325        bytes.push(self.intent.to_u8());
326        bytes.extend_from_slice(&self.author_pubkey);
327        bytes.extend_from_slice(&self.author_signature);
328        
329        bytes
330    }
331    
332    fn to_bytes_v2(&self) -> Vec<u8> {
333        let mut bytes = Vec::with_capacity(ARTIFACT_SIZE_V2);
334        
335        bytes.extend_from_slice(MAGIC);  // 4
336        bytes.extend_from_slice(&VERSION_V2.to_le_bytes());  // 2
337        bytes.push(self.reserved_a);  // 1
338        bytes.extend_from_slice(&self.artifact_id.0);  // 16
339        bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());  // 8
340        bytes.push(self.intent.to_u8());  // 1
341        bytes.extend_from_slice(&self.author_pubkey);  // 32
342        bytes.extend_from_slice(&self.note_hash);  // 32
343        bytes.extend_from_slice(&self.author_signature);  // 64
344        bytes.extend_from_slice(&self.seal_pubkey);  // 32
345        bytes.extend_from_slice(&self.seal_signature);  // 64
346        
347        bytes
348    }
349
350    /// Write artifact to a writer
351    pub fn write_to<W: Write>(&self, writer: &mut W) -> Result<()> {
352        writer.write_all(&self.to_bytes())?;
353        Ok(())
354    }
355
356    /// Read artifact from a reader
357    pub fn read_from<R: Read>(reader: &mut R) -> Result<Self> {
358        let mut bytes = Vec::new();
359        reader.read_to_end(&mut bytes)?;
360        Self::from_bytes(&bytes)
361    }
362
363    /// Get author key fingerprint
364    pub fn author_fingerprint(&self) -> KeyFingerprint {
365        KeyFingerprint::new(&self.author_pubkey)
366    }
367    
368    /// Get seal key fingerprint (V2 only)
369    pub fn seal_fingerprint(&self) -> Option<KeyFingerprint> {
370        if self.version == ArtifactVersion::V2 && self.seal_pubkey != ZERO_PUBKEY {
371            Some(KeyFingerprint::new(&self.seal_pubkey))
372        } else {
373            None
374        }
375    }
376    
377    /// Check if artifact is sealed
378    pub fn is_sealed(&self) -> bool {
379        self.state() == ArtifactState::Sealed
380    }
381    
382    /// Check if artifact has author signature
383    pub fn has_author_signature(&self) -> bool {
384        self.author_signature != ZERO_SIGNATURE
385    }
386    
387    /// Check if artifact has a signature (legacy compatibility)
388    pub fn has_signature(&self) -> bool {
389        self.has_author_signature()
390    }
391    
392    /// Check if artifact has an author public key (keyless draft = all zeros)
393    pub fn has_author_pubkey(&self) -> bool {
394        self.author_pubkey != ZERO_PUBKEY
395    }
396    
397    /// Check if note hash is present (V2 only)
398    pub fn has_note_hash(&self) -> bool {
399        self.version == ArtifactVersion::V2 && self.note_hash != ZERO_HASH
400    }
401    
402    /// Determine artifact state
403    pub fn state(&self) -> ArtifactState {
404        if !self.has_author_signature() {
405            ArtifactState::Draft
406        } else if self.is_sealed_raw() {
407            ArtifactState::Sealed
408        } else {
409            ArtifactState::Signed
410        }
411    }
412
413    /// Internal raw sealed check (used by state() to avoid circular call with is_sealed())
414    fn is_sealed_raw(&self) -> bool {
415        match self.version {
416            ArtifactVersion::V1 => (self.flags & FLAG_SEALED) != 0,
417            ArtifactVersion::V2 => self.seal_signature != ZERO_SIGNATURE,
418        }
419    }
420    
421    /// Set seal flag (V1 only - returns new artifact with flag set)
422    pub fn with_seal_flag(mut self) -> Self {
423        if self.version == ArtifactVersion::V1 {
424            self.flags |= FLAG_SEALED;
425        }
426        self
427    }
428    
429    /// Clear seal flag (V1 only - returns new artifact with flag cleared)
430    pub fn without_seal_flag(mut self) -> Self {
431        if self.version == ArtifactVersion::V1 {
432            self.flags &= !FLAG_SEALED;
433        }
434        self
435    }
436    
437    /// Remove author signature
438    pub fn without_author_signature(mut self) -> Self {
439        self.author_signature = ZERO_SIGNATURE;
440        self
441    }
442    
443    /// Remove signature (legacy compatibility)
444    pub fn without_signature(self) -> Self {
445        self.without_author_signature()
446    }
447    
448    /// Remove seal signature (V2 only)
449    pub fn without_seal_signature(mut self) -> Self {
450        if self.version == ArtifactVersion::V2 {
451            self.seal_signature = ZERO_SIGNATURE;
452            self.seal_pubkey = ZERO_PUBKEY;
453        }
454        self
455    }
456    
457    /// Add seal signature (V2 only)
458    pub fn with_seal(mut self, seal_pubkey: [u8; 32], seal_signature: [u8; 64]) -> Self {
459        if self.version == ArtifactVersion::V2 {
460            self.seal_pubkey = seal_pubkey;
461            self.seal_signature = seal_signature;
462        }
463        self
464    }
465}
466
467// --- Type-level artifact state machine ---
468
469/// Phantom state type: no author signature present
470pub struct Draft;
471/// Phantom state type: author-signed but not sealed
472pub struct Signed;
473/// Phantom state type: dual-signed (author + seal)
474pub struct Sealed;
475
476/// Zero-cost wrapper that encodes artifact lifecycle state in the type system.
477pub struct TypedArtifact<S> {
478    pub inner: Artifact,
479    _state: std::marker::PhantomData<S>,
480}
481
482fn state_name(s: ArtifactState) -> &'static str {
483    match s {
484        ArtifactState::Draft => "Draft",
485        ArtifactState::Signed => "Signed",
486        ArtifactState::Sealed => "Sealed",
487    }
488}
489
490impl TypedArtifact<Draft> {
491    pub fn new(artifact: Artifact) -> Result<Self> {
492        if artifact.state() != ArtifactState::Draft {
493            return Err(Error::InvalidState {
494                expected: "Draft",
495                actual: state_name(artifact.state()),
496            });
497        }
498        Ok(Self { inner: artifact, _state: std::marker::PhantomData })
499    }
500
501    /// Transition Draft → Signed. Consumes self; caller supplies the post-sign artifact.
502    pub fn into_signed(self, artifact: Artifact) -> Result<TypedArtifact<Signed>> {
503        if artifact.state() != ArtifactState::Signed {
504            return Err(Error::InvalidState {
505                expected: "Signed",
506                actual: state_name(artifact.state()),
507            });
508        }
509        Ok(TypedArtifact { inner: artifact, _state: std::marker::PhantomData })
510    }
511}
512
513impl TypedArtifact<Signed> {
514    pub fn new(artifact: Artifact) -> Result<Self> {
515        if artifact.state() != ArtifactState::Signed {
516            return Err(Error::InvalidState {
517                expected: "Signed",
518                actual: state_name(artifact.state()),
519            });
520        }
521        Ok(Self { inner: artifact, _state: std::marker::PhantomData })
522    }
523
524    /// Transition Signed → Sealed. Consumes self; caller supplies the post-seal artifact.
525    pub fn into_sealed(self, artifact: Artifact) -> Result<TypedArtifact<Sealed>> {
526        if artifact.state() != ArtifactState::Sealed {
527            return Err(Error::InvalidState {
528                expected: "Sealed",
529                actual: state_name(artifact.state()),
530            });
531        }
532        Ok(TypedArtifact { inner: artifact, _state: std::marker::PhantomData })
533    }
534}
535
536impl TypedArtifact<Sealed> {
537    pub fn new(artifact: Artifact) -> Result<Self> {
538        if artifact.state() != ArtifactState::Sealed {
539            return Err(Error::InvalidState {
540                expected: "Sealed",
541                actual: state_name(artifact.state()),
542            });
543        }
544        Ok(Self { inner: artifact, _state: std::marker::PhantomData })
545    }
546}
547
548impl<S> TypedArtifact<S> {
549    pub fn artifact(&self) -> &Artifact {
550        &self.inner
551    }
552}
553
554/// Builder for creating new artifacts
555pub struct ArtifactBuilder {
556    version: ArtifactVersion,
557    artifact_id: ArtifactId,
558    timestamp: DateTime<Utc>,
559    intent: Intent,
560    author_pubkey: [u8; 32],
561    flags: u8,  // V1 only
562    note_hash: [u8; 32],  // V2 only
563}
564
565impl ArtifactBuilder {
566    /// Create v1 artifact builder
567    pub fn new_v1(intent: Intent, author_pubkey: [u8; 32]) -> Self {
568        Self {
569            version: ArtifactVersion::V1,
570            artifact_id: ArtifactId::new(),
571            timestamp: Utc::now(),
572            intent,
573            author_pubkey,
574            flags: 0,
575            note_hash: ZERO_HASH,
576        }
577    }
578    
579    /// Create v2 artifact builder
580    pub fn new_v2(intent: Intent, author_pubkey: [u8; 32], note_hash: [u8; 32]) -> Self {
581        Self {
582            version: ArtifactVersion::V2,
583            artifact_id: ArtifactId::new(),
584            timestamp: Utc::now(),
585            intent,
586            author_pubkey,
587            flags: 0,
588            note_hash,
589        }
590    }
591    
592    /// Create artifact builder (defaults to v1 for backward compatibility)
593    pub fn new(intent: Intent, author_pubkey: [u8; 32]) -> Self {
594        Self::new_v1(intent, author_pubkey)
595    }
596
597    pub fn with_artifact_id(mut self, id: ArtifactId) -> Self {
598        self.artifact_id = id;
599        self
600    }
601
602    pub fn with_timestamp(mut self, timestamp: DateTime<Utc>) -> Self {
603        self.timestamp = timestamp;
604        self
605    }
606
607    pub fn with_flags(mut self, flags: u8) -> Self {
608        self.flags = flags;
609        self
610    }
611    
612    pub fn with_note_hash(mut self, note_hash: [u8; 32]) -> Self {
613        self.note_hash = note_hash;
614        self
615    }
616
617    /// Build unsigned artifact (for signing)
618    pub fn build_unsigned(&self) -> UnsignedArtifact {
619        UnsignedArtifact {
620            version: self.version,
621            flags: self.flags,
622            artifact_id: self.artifact_id,
623            timestamp: self.timestamp,
624            intent: self.intent,
625            author_pubkey: self.author_pubkey,
626            note_hash: self.note_hash,
627        }
628    }
629}
630
631/// Unsigned artifact (before signing)
632pub struct UnsignedArtifact {
633    pub version: ArtifactVersion,
634    pub flags: u8,  // V1 only
635    pub artifact_id: ArtifactId,
636    pub timestamp: DateTime<Utc>,
637    pub intent: Intent,
638    pub author_pubkey: [u8; 32],
639    pub note_hash: [u8; 32],  // V2 only
640}
641
642impl UnsignedArtifact {
643    /// Get bytes to sign (author signature)
644    pub fn author_signing_bytes(&self) -> Vec<u8> {
645        match self.version {
646            ArtifactVersion::V1 => {
647                // V1: 63 bytes (FLAGS excluded)
648                let mut bytes = Vec::with_capacity(63);
649                bytes.extend_from_slice(MAGIC);
650                bytes.extend_from_slice(&VERSION_V1.to_le_bytes());
651                // FLAGS NOT INCLUDED
652                bytes.extend_from_slice(&self.artifact_id.0);
653                bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());
654                bytes.push(self.intent.to_u8());
655                bytes.extend_from_slice(&self.author_pubkey);
656                bytes
657            },
658            ArtifactVersion::V2 => {
659                // V2: 103 bytes
660                let mut bytes = Vec::with_capacity(103);
661                bytes.extend_from_slice(MAGIC);
662                bytes.extend_from_slice(&VERSION_V2.to_le_bytes());
663                bytes.push(0u8);  // reserved_a
664                bytes.extend_from_slice(&self.artifact_id.0);
665                bytes.extend_from_slice(&(self.timestamp.timestamp() as u64).to_le_bytes());
666                bytes.push(self.intent.to_u8());
667                bytes.extend_from_slice(&self.author_pubkey);
668                bytes.extend_from_slice(&self.note_hash);
669                bytes
670            },
671        }
672    }
673    
674    /// Attach author signature and create complete artifact
675    pub fn with_author_signature(self, signature: [u8; 64]) -> Artifact {
676        Artifact {
677            version: self.version,
678            artifact_id: self.artifact_id,
679            timestamp: self.timestamp,
680            intent: self.intent,
681            author_pubkey: self.author_pubkey,
682            author_signature: signature,
683            flags: self.flags,
684            reserved_a: 0,
685            note_hash: self.note_hash,
686            seal_pubkey: ZERO_PUBKEY,
687            seal_signature: ZERO_SIGNATURE,
688        }
689    }
690    
691    /// Attach signature (legacy compatibility)
692    pub fn with_signature(self, signature: [u8; 64]) -> Artifact {
693        self.with_author_signature(signature)
694    }
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use crate::{crypto, Intent};
701    use ed25519_dalek::SigningKey;
702    use rand::rngs::OsRng;
703
704    fn make_author_key() -> SigningKey {
705        SigningKey::generate(&mut OsRng)
706    }
707
708    fn make_seal_key() -> SigningKey {
709        SigningKey::generate(&mut OsRng)
710    }
711
712    // --- V1 roundtrip ---
713
714    #[test]
715    fn test_v1_roundtrip() {
716        let author_key = make_author_key();
717        let author_pubkey = author_key.verifying_key().to_bytes();
718
719        let unsigned = ArtifactBuilder::new_v1(Intent::Lab, author_pubkey).build_unsigned();
720        let sig = crypto::sign(&author_key, &unsigned.author_signing_bytes());
721        let artifact = unsigned.with_signature(sig);
722
723        let parsed = Artifact::from_bytes(&artifact.to_bytes()).unwrap();
724
725        assert_eq!(parsed.version, ArtifactVersion::V1);
726        assert_eq!(parsed.artifact_id, artifact.artifact_id);
727        assert_eq!(parsed.intent, artifact.intent);
728        assert_eq!(parsed.author_pubkey, artifact.author_pubkey);
729        assert_eq!(parsed.author_signature, artifact.author_signature);
730        assert_eq!(parsed.flags, artifact.flags);
731    }
732
733    // --- V2 roundtrip ---
734
735    #[test]
736    fn test_v2_roundtrip_unsigned() {
737        let author_key = make_author_key();
738        let author_pubkey = author_key.verifying_key().to_bytes();
739        let note_hash = [0xabu8; 32];
740
741        let unsigned = ArtifactBuilder::new_v2(Intent::Lab, author_pubkey, note_hash).build_unsigned();
742        let sig = crypto::sign(&author_key, &unsigned.author_signing_bytes());
743        let artifact = unsigned.with_signature(sig);
744
745        let parsed = Artifact::from_bytes(&artifact.to_bytes()).unwrap();
746
747        assert_eq!(parsed.version, ArtifactVersion::V2);
748        assert_eq!(parsed.artifact_id, artifact.artifact_id);
749        assert_eq!(parsed.intent, artifact.intent);
750        assert_eq!(parsed.author_pubkey, artifact.author_pubkey);
751        assert_eq!(parsed.author_signature, artifact.author_signature);
752        assert_eq!(parsed.note_hash, note_hash);
753        assert_eq!(parsed.seal_pubkey, ZERO_PUBKEY);
754        assert_eq!(parsed.seal_signature, ZERO_SIGNATURE);
755    }
756
757    #[test]
758    fn test_v2_roundtrip_sealed() {
759        let author_key = make_author_key();
760        let seal_key = make_seal_key();
761        let author_pubkey = author_key.verifying_key().to_bytes();
762        let seal_pubkey = seal_key.verifying_key().to_bytes();
763        let note_hash = [0xbcu8; 32];
764
765        let unsigned = ArtifactBuilder::new_v2(Intent::Lab, author_pubkey, note_hash).build_unsigned();
766        let author_sig = crypto::sign(&author_key, &unsigned.author_signing_bytes());
767        let author_signed = unsigned.with_signature(author_sig);
768
769        let pre_seal = author_signed.with_seal(seal_pubkey, [0u8; 64]);
770        let seal_sig = crypto::sign(&seal_key, &pre_seal.seal_signing_bytes().unwrap());
771        let sealed = pre_seal.with_seal(seal_pubkey, seal_sig);
772
773        let parsed = Artifact::from_bytes(&sealed.to_bytes()).unwrap();
774
775        assert_eq!(parsed.version, ArtifactVersion::V2);
776        assert_eq!(parsed.note_hash, note_hash);
777        assert_eq!(parsed.author_pubkey, author_pubkey);
778        assert_eq!(parsed.author_signature, sealed.author_signature);
779        assert_eq!(parsed.seal_pubkey, seal_pubkey);
780        assert_eq!(parsed.seal_signature, sealed.seal_signature);
781        // Verify seal signature actually validates (not just stored correctly)
782        assert!(crypto::verify(&seal_pubkey, &parsed.seal_signing_bytes().unwrap(), &parsed.seal_signature).is_ok());
783    }
784
785    // --- State machine ---
786
787    #[test]
788    fn test_v2_state_draft() {
789        let author_key = make_author_key();
790        let author_pubkey = author_key.verifying_key().to_bytes();
791        let unsigned = ArtifactBuilder::new_v2(Intent::Lab, author_pubkey, ZERO_HASH).build_unsigned();
792        let artifact = unsigned.with_signature(ZERO_SIGNATURE);
793        assert_eq!(artifact.state(), ArtifactState::Draft);
794        assert!(!artifact.has_author_signature());
795        assert!(!artifact.is_sealed());
796    }
797
798    #[test]
799    fn test_v2_state_signed() {
800        let author_key = make_author_key();
801        let author_pubkey = author_key.verifying_key().to_bytes();
802        let unsigned = ArtifactBuilder::new_v2(Intent::Lab, author_pubkey, ZERO_HASH).build_unsigned();
803        let sig = crypto::sign(&author_key, &unsigned.author_signing_bytes());
804        let artifact = unsigned.with_signature(sig);
805
806        assert_eq!(artifact.state(), ArtifactState::Signed);
807        assert!(artifact.has_author_signature());
808        assert!(!artifact.is_sealed());
809    }
810
811    #[test]
812    fn test_v2_state_sealed() {
813        let author_key = make_author_key();
814        let seal_key = make_seal_key();
815        let author_pubkey = author_key.verifying_key().to_bytes();
816        let seal_pubkey = seal_key.verifying_key().to_bytes();
817
818        let unsigned = ArtifactBuilder::new_v2(Intent::Lab, author_pubkey, ZERO_HASH).build_unsigned();
819        let author_sig = crypto::sign(&author_key, &unsigned.author_signing_bytes());
820        let author_signed = unsigned.with_signature(author_sig);
821        let pre_seal = author_signed.with_seal(seal_pubkey, [0u8; 64]);
822        let seal_sig = crypto::sign(&seal_key, &pre_seal.seal_signing_bytes().unwrap());
823        let sealed = pre_seal.with_seal(seal_pubkey, seal_sig);
824
825        assert_eq!(sealed.state(), ArtifactState::Sealed);
826        assert!(sealed.has_author_signature());
827        assert!(sealed.is_sealed());
828    }
829
830    // --- Signature binding ---
831
832    #[test]
833    fn test_v2_author_sig_covers_note_hash() {
834        let author_key = make_author_key();
835        let author_pubkey = author_key.verifying_key().to_bytes();
836        let note_hash = [0x11u8; 32];
837
838        let unsigned = ArtifactBuilder::new_v2(Intent::Lab, author_pubkey, note_hash).build_unsigned();
839        let sig = crypto::sign(&author_key, &unsigned.author_signing_bytes());
840        let mut artifact = unsigned.with_signature(sig);
841        artifact.note_hash = [0x22u8; 32]; // tamper
842
843        let verify_bytes = artifact.author_signing_bytes();
844        assert!(crypto::verify(&author_pubkey, &verify_bytes, &artifact.author_signature).is_err());
845    }
846
847    #[test]
848    fn test_v2_seal_sig_covers_author_sig() {
849        let author_key = make_author_key();
850        let seal_key = make_seal_key();
851        let author_pubkey = author_key.verifying_key().to_bytes();
852        let seal_pubkey = seal_key.verifying_key().to_bytes();
853
854        let unsigned = ArtifactBuilder::new_v2(Intent::Lab, author_pubkey, ZERO_HASH).build_unsigned();
855        let author_sig = crypto::sign(&author_key, &unsigned.author_signing_bytes());
856        let author_signed = unsigned.with_signature(author_sig);
857        let pre_seal = author_signed.with_seal(seal_pubkey, [0u8; 64]);
858        let seal_sig = crypto::sign(&seal_key, &pre_seal.seal_signing_bytes().unwrap());
859        let mut sealed = pre_seal.with_seal(seal_pubkey, seal_sig);
860        sealed.author_signature = [0xffu8; 64]; // tamper
861
862        let verify_bytes = sealed.seal_signing_bytes().unwrap();
863        assert!(crypto::verify(&seal_pubkey, &verify_bytes, &sealed.seal_signature).is_err());
864    }
865
866    #[test]
867    fn test_v2_seal_key_can_differ_from_author_key() {
868        let author_key = make_author_key();
869        let seal_key = make_seal_key();
870        assert_ne!(
871            author_key.verifying_key().to_bytes(),
872            seal_key.verifying_key().to_bytes()
873        );
874    }
875
876    // --- Unsign/unseal ---
877
878    #[test]
879    fn test_v2_without_author_signature() {
880        let author_key = make_author_key();
881        let author_pubkey = author_key.verifying_key().to_bytes();
882        let unsigned = ArtifactBuilder::new_v2(Intent::Lab, author_pubkey, ZERO_HASH).build_unsigned();
883        let sig = crypto::sign(&author_key, &unsigned.author_signing_bytes());
884        let artifact = unsigned.with_signature(sig).without_author_signature();
885
886        assert_eq!(artifact.author_signature, ZERO_SIGNATURE);
887        assert_eq!(artifact.state(), ArtifactState::Draft);
888    }
889
890    #[test]
891    fn test_v2_without_seal_signature() {
892        let author_key = make_author_key();
893        let seal_key = make_seal_key();
894        let author_pubkey = author_key.verifying_key().to_bytes();
895        let seal_pubkey = seal_key.verifying_key().to_bytes();
896
897        let unsigned = ArtifactBuilder::new_v2(Intent::Lab, author_pubkey, ZERO_HASH).build_unsigned();
898        let author_sig = crypto::sign(&author_key, &unsigned.author_signing_bytes());
899        let author_signed = unsigned.with_signature(author_sig);
900        let pre_seal = author_signed.with_seal(seal_pubkey, [0u8; 64]);
901        let seal_sig = crypto::sign(&seal_key, &pre_seal.seal_signing_bytes().unwrap());
902        let sealed = pre_seal.with_seal(seal_pubkey, seal_sig);
903        let unsealed = sealed.without_seal_signature();
904
905        assert_eq!(unsealed.seal_signature, ZERO_SIGNATURE);
906        assert_eq!(unsealed.seal_pubkey, ZERO_PUBKEY);
907        assert_eq!(unsealed.state(), ArtifactState::Signed);
908        assert!(unsealed.has_author_signature());
909    }
910
911    // --- Parse errors ---
912
913    #[test]
914    fn test_invalid_magic() {
915        let mut bytes = vec![0u8; ARTIFACT_SIZE_V1];
916        bytes[0..4].copy_from_slice(b"NOPE");
917        assert!(matches!(Artifact::from_bytes(&bytes), Err(Error::InvalidMagic)));
918    }
919
920    #[test]
921    fn test_file_too_small() {
922        let bytes = vec![0u8; 5]; // below 6-byte minimum for magic+version
923        assert!(matches!(Artifact::from_bytes(&bytes), Err(Error::FileTooSmall { .. })));
924    }
925
926    #[test]
927    fn test_v1_wrong_size_rejected() {
928        let mut bytes = vec![0u8; ARTIFACT_SIZE_V1 - 1];
929        bytes[0..4].copy_from_slice(MAGIC);
930        bytes[4..6].copy_from_slice(&VERSION_V1.to_le_bytes());
931        assert!(Artifact::from_bytes(&bytes).is_err());
932    }
933
934    #[test]
935    fn test_v2_wrong_size_rejected() {
936        let mut bytes = vec![0u8; ARTIFACT_SIZE_V2 - 1];
937        bytes[0..4].copy_from_slice(MAGIC);
938        bytes[4..6].copy_from_slice(&VERSION_V2.to_le_bytes());
939        assert!(Artifact::from_bytes(&bytes).is_err());
940    }
941}
942