Skip to main content

common/mount/
manifest.rs

1//! # Manifest
2//!
3//! The manifest is the root metadata structure for a bucket. It contains:
4//!
5//! - **Identity**: UUID and friendly name
6//! - **Access control**: Map of principals to their shares
7//! - **Content**: Links to the entry node and pin set
8//! - **History**: Link to previous manifest version and height in the chain
9//! - **Publication state**: Optional plaintext secret for public read access
10//!
11//! ## Encryption Model
12//!
13//! - **Owners** have an encrypted [`SecretShare`] that they can decrypt with their private key
14//! - **Mirrors** have no individual share; they use [`Manifest::public`] when available
15//! - **Publishing** stores the bucket's secret in plaintext, making it readable by anyone with the manifest
16//!
17//! ## Versioning
18//!
19//! Each modification creates a new manifest with:
20//! - `previous` pointing to the prior manifest's CID
21//! - `height` incremented by 1
22//!
23//! This forms an immutable version chain for history traversal.
24
25use std::collections::BTreeMap;
26
27use serde::{Deserialize, Serialize};
28use uuid::Uuid;
29
30use crate::crypto::{PublicKey, Secret, SecretKey, SecretShare, Signature};
31use crate::linked_data::{BlockEncoded, CodecError, DagCborCodec, Link};
32use crate::version::Version;
33
34use super::principal::{Principal, PrincipalRole};
35
36/// Errors that can occur during manifest operations.
37#[derive(Debug, thiserror::Error)]
38pub enum ManifestError {
39    #[error("codec error: {0}")]
40    Codec(#[from] CodecError),
41    #[error("signature verification failed")]
42    SignatureVerificationFailed,
43}
44
45/// A principal's share of bucket access.
46///
47/// Combines a [`Principal`] (identity + role) with an optional encrypted secret share.
48/// The share structure differs by role:
49///
50/// - **Owners**: Always have `Some(SecretShare)` encrypted to their public key
51/// - **Mirrors**: Always have `None`; use the manifest's `public` secret instead
52#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
53pub struct Share {
54    principal: Principal,
55    /// The encrypted share of the bucket's secret key.
56    /// Only owners have this; mirrors use the manifest's public secret instead.
57    share: Option<SecretShare>,
58}
59
60impl Share {
61    /// Create a new owner share with an encrypted secret.
62    pub fn new_owner(share: SecretShare, public_key: PublicKey) -> Self {
63        Self {
64            principal: Principal {
65                role: PrincipalRole::Owner,
66                identity: public_key,
67            },
68            share: Some(share),
69        }
70    }
71
72    /// Create a new mirror share.
73    ///
74    /// Mirrors don't have individual encrypted shares. They use the manifest's
75    /// `public` field for decryption once the bucket is published.
76    pub fn new_mirror(public_key: PublicKey) -> Self {
77        Self {
78            principal: Principal {
79                role: PrincipalRole::Mirror,
80                identity: public_key,
81            },
82            share: None,
83        }
84    }
85
86    /* Getters */
87
88    /// Get the principal (identity and role).
89    pub fn principal(&self) -> &Principal {
90        &self.principal
91    }
92
93    /// Get the encrypted secret share.
94    ///
95    /// Returns `Some` for owners, `None` for mirrors.
96    pub fn share(&self) -> Option<&SecretShare> {
97        self.share.as_ref()
98    }
99
100    /// Get the principal's role.
101    pub fn role(&self) -> &PrincipalRole {
102        &self.principal.role
103    }
104
105    /* Setters */
106
107    /// Set the encrypted secret share.
108    pub fn set_share(&mut self, share: SecretShare) {
109        self.share = Some(share);
110    }
111}
112
113/// Map of hex-encoded public keys to their shares.
114///
115/// Uses `String` keys (hex-encoded [`PublicKey`]) for CBOR serialization compatibility.
116pub type Shares = BTreeMap<String, Share>;
117
118/// The root metadata structure for a bucket.
119///
120/// A manifest contains everything needed to access and verify a bucket:
121///
122/// - **Identity**: Global UUID and human-readable name
123/// - **Access control**: Principal shares for decryption
124/// - **Content pointers**: Links to entry node, pin set, and crdt op log
125/// - **Version chain**: Previous link and height for history
126/// - **Publication**: Optional plaintext secret for public access
127///
128/// # Serialization
129///
130/// Manifests are serialized using DAG-CBOR and stored as content-addressed blobs.
131/// The manifest's CID serves as the bucket's current state identifier.
132///
133/// # Example
134///
135/// ```ignore
136/// let manifest = Manifest::new(
137///     Uuid::new_v4(),
138///     "my-bucket".to_string(),
139///     owner_public_key,
140///     secret_share,
141///     entry_link,
142///     pins_link,
143///     0, // initial height
144/// );
145/// ```
146#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
147pub struct Manifest {
148    /// Global unique identifier for this bucket.
149    id: Uuid,
150    /// Human-readable name for display.
151    name: String,
152    /// Height in the version chain (0 for initial, increments on each update).
153    height: u64,
154    /// Software version for compatibility checking.
155    version: Version,
156    /// Map of principal public keys (hex) to their shares.
157    shares: Shares,
158    /// Link to the root [`Node`](super::Node) of the file tree.
159    entry: Link,
160    /// Link to the [`Pins`](super::Pins) blob hash set.
161    pins: Link,
162    /// Link to the previous manifest version (forms history chain).
163    previous: Option<Link>,
164    /// Optional link to the encrypted path operations log (CRDT).
165    #[serde(default, skip_serializing_if = "Option::is_none")]
166    ops_log: Option<Link>,
167    /// Plaintext secret for public read access.
168    ///
169    /// When set, anyone with the manifest can decrypt bucket contents.
170    /// Publishing is opt-in per version via `save(publish: true)`.
171    #[serde(default, skip_serializing_if = "Option::is_none")]
172    public: Option<Secret>,
173    /// Public key of the peer who signed this manifest.
174    ///
175    /// Set when the manifest is signed via [`Manifest::sign`].
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    author: Option<PublicKey>,
178    /// Ed25519 signature over the manifest contents.
179    ///
180    /// The signature covers all fields except `signature` itself (see [`Manifest::signable_bytes`]).
181    #[serde(default, skip_serializing_if = "Option::is_none")]
182    signature: Option<Signature>,
183}
184
185impl BlockEncoded<DagCborCodec> for Manifest {}
186
187impl Manifest {
188    /// Create a new manifest with an initial owner.
189    ///
190    /// # Arguments
191    ///
192    /// * `id` - Global unique identifier for the bucket
193    /// * `name` - Human-readable display name
194    /// * `owner` - Public key of the initial owner
195    /// * `share` - Encrypted secret share for the owner
196    /// * `entry` - Link to the root node
197    /// * `pins` - Link to the pin set
198    /// * `height` - Version chain height (usually 0 for new buckets)
199    pub fn new(
200        id: Uuid,
201        name: String,
202        owner: PublicKey,
203        share: SecretShare,
204        entry: Link,
205        pins: Link,
206        height: u64,
207    ) -> Self {
208        Manifest {
209            id,
210            name,
211            shares: BTreeMap::from([(
212                owner.to_hex(),
213                Share {
214                    principal: Principal {
215                        role: PrincipalRole::Owner,
216                        identity: owner,
217                    },
218                    share: Some(share),
219                },
220            )]),
221            entry,
222            pins,
223            previous: None,
224            height,
225            version: Version::default(),
226            ops_log: None,
227            public: None,
228            author: None,
229            signature: None,
230        }
231    }
232
233    /* Getters */
234
235    /// Get the bucket's unique identifier.
236    pub fn id(&self) -> &Uuid {
237        &self.id
238    }
239
240    /// Get the bucket's display name.
241    pub fn name(&self) -> &str {
242        &self.name
243    }
244
245    /// Get the software version.
246    pub fn version(&self) -> &Version {
247        &self.version
248    }
249
250    /// Get the entry node link.
251    pub fn entry(&self) -> &Link {
252        &self.entry
253    }
254
255    /// Get the pins link.
256    pub fn pins(&self) -> &Link {
257        &self.pins
258    }
259
260    /// Get the previous manifest link.
261    pub fn previous(&self) -> &Option<Link> {
262        &self.previous
263    }
264
265    /// Get the version chain height.
266    pub fn height(&self) -> u64 {
267        self.height
268    }
269
270    /// Get the operations log link.
271    pub fn ops_log(&self) -> Option<&Link> {
272        self.ops_log.as_ref()
273    }
274
275    /// Get all shares.
276    pub fn shares(&self) -> &BTreeMap<String, Share> {
277        &self.shares
278    }
279
280    /// Get mutable access to shares.
281    pub fn shares_mut(&mut self) -> &mut BTreeMap<String, Share> {
282        &mut self.shares
283    }
284
285    /// Get a principal's share by their public key.
286    pub fn get_share(&self, public_key: &PublicKey) -> Option<&Share> {
287        self.shares.get(&public_key.to_hex())
288    }
289
290    /// Get all peer public keys from shares.
291    pub fn get_peer_ids(&self) -> Vec<PublicKey> {
292        self.shares
293            .iter()
294            .filter_map(|(key_hex, _)| PublicKey::from_hex(key_hex).ok())
295            .collect()
296    }
297
298    /// Get all shares with a specific role.
299    pub fn get_shares_by_role(&self, role: PrincipalRole) -> Vec<&Share> {
300        self.shares.values().filter(|s| *s.role() == role).collect()
301    }
302
303    /// Check if the bucket is published.
304    ///
305    /// Published buckets have their secret stored in plaintext, allowing
306    /// anyone with the manifest to decrypt contents.
307    pub fn is_published(&self) -> bool {
308        self.public.is_some()
309    }
310
311    /// Get the public secret if available.
312    pub fn public(&self) -> Option<&Secret> {
313        self.public.as_ref()
314    }
315
316    /// Get the author (signer's public key) if the manifest is signed.
317    pub fn author(&self) -> Option<&PublicKey> {
318        self.author.as_ref()
319    }
320
321    /// Get the signature if the manifest is signed.
322    pub fn signature(&self) -> Option<&Signature> {
323        self.signature.as_ref()
324    }
325
326    /// Check if the manifest has been signed.
327    pub fn is_signed(&self) -> bool {
328        self.author.is_some() && self.signature.is_some()
329    }
330
331    /* Setters */
332
333    /// Set the entry node link.
334    pub fn set_entry(&mut self, entry: Link) {
335        self.entry = entry;
336    }
337
338    /// Set the pins link.
339    pub fn set_pins(&mut self, pins_link: Link) {
340        self.pins = pins_link;
341    }
342
343    /// Set the previous manifest link.
344    pub fn set_previous(&mut self, previous: Link) {
345        self.previous = Some(previous);
346    }
347
348    /// Set the version chain height.
349    pub fn set_height(&mut self, height: u64) {
350        self.height = height;
351    }
352
353    /// Set the operations log link.
354    pub fn set_ops_log(&mut self, link: Link) {
355        self.ops_log = Some(link);
356    }
357
358    /// Clear the operations log link.
359    ///
360    /// This should be called when creating a new version from an existing manifest,
361    /// since each version has its own independent ops_log.
362    pub fn clear_ops_log(&mut self) {
363        self.ops_log = None;
364    }
365
366    /// Add a share to the manifest.
367    ///
368    /// Use [`Share::new_owner`] or [`Share::new_mirror`] to construct the share.
369    pub fn add_share(&mut self, share: Share) {
370        let key = share.principal().identity.to_hex();
371        self.shares.insert(key, share);
372    }
373
374    /// Publish the bucket by storing the secret in plaintext.
375    ///
376    /// **Warning**: Once published, this version's secret is exposed
377    /// and anyone with the manifest can decrypt bucket contents.
378    pub fn publish(&mut self, secret: &Secret) {
379        self.public = Some(secret.clone());
380    }
381
382    /// Unpublish the bucket by clearing the public secret.
383    ///
384    /// This removes public read access. Mirrors will no longer be able
385    /// to decrypt bucket contents until republished.
386    pub fn unpublish(&mut self) {
387        self.public = None;
388    }
389
390    /* Signing */
391
392    /// Sign this manifest with the given secret key.
393    ///
394    /// Sets the `author` field to the public key and `signature` to the Ed25519
395    /// signature over the manifest's signable bytes.
396    ///
397    /// # Errors
398    ///
399    /// Returns an error if the manifest cannot be serialized for signing.
400    pub fn sign(&mut self, secret_key: &SecretKey) -> Result<(), ManifestError> {
401        self.author = Some(secret_key.public());
402        self.signature = None; // Clear any existing signature before computing signable_bytes
403        let bytes = self.signable_bytes()?;
404        let signature = secret_key.sign(&bytes);
405        self.signature = Some(signature);
406        Ok(())
407    }
408
409    /// Verify the manifest's signature.
410    ///
411    /// Returns `Ok(true)` if the signature is valid, `Ok(false)` if the manifest
412    /// is unsigned (no author or signature), and an error if verification fails.
413    ///
414    /// # Errors
415    ///
416    /// Returns an error if:
417    /// - The manifest cannot be serialized for verification
418    /// - The signature is invalid (tampered or wrong key)
419    pub fn verify_signature(&self) -> Result<bool, ManifestError> {
420        let (author, signature) = match (self.author.as_ref(), self.signature.as_ref()) {
421            (Some(a), Some(s)) => (a, s),
422            _ => return Ok(false), // Not signed
423        };
424
425        let bytes = self.signable_bytes()?;
426        author
427            .verify(&bytes, signature)
428            .map_err(|_| ManifestError::SignatureVerificationFailed)?;
429        Ok(true)
430    }
431
432    /// Get the bytes to be signed.
433    ///
434    /// Returns the DAG-CBOR serialization of the manifest with `signature` set to `None`.
435    /// This ensures the signature covers all fields except itself.
436    fn signable_bytes(&self) -> Result<Vec<u8>, ManifestError> {
437        let mut signable = self.clone();
438        signable.signature = None; // Exclude signature field
439        Ok(signable.encode()?) // DAG-CBOR serialize
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446    #[allow(unused_imports)]
447    use crate::crypto::{PublicKey, Secret};
448    use crate::linked_data::Link;
449
450    fn create_test_manifest() -> Manifest {
451        let secret_key = SecretKey::generate();
452        let public_key = secret_key.public();
453        let share = SecretShare::default();
454        let entry_link = Link::default();
455        let pins_link = Link::default();
456
457        Manifest::new(
458            uuid::Uuid::new_v4(),
459            "test-bucket".to_string(),
460            public_key,
461            share,
462            entry_link,
463            pins_link,
464            0,
465        )
466    }
467
468    #[test]
469    fn test_share_serialize() {
470        use ipld_core::codec::Codec;
471        use serde_ipld_dagcbor::codec::DagCborCodec;
472
473        let share = SecretShare::default();
474
475        // Try to encode/decode just the Share
476        let encoded = DagCborCodec::encode_to_vec(&share).unwrap();
477        let decoded: SecretShare = DagCborCodec::decode_from_slice(&encoded).unwrap();
478
479        assert_eq!(share, decoded);
480    }
481
482    #[test]
483    fn test_principal_serialize() {
484        use ipld_core::codec::Codec;
485        use serde_ipld_dagcbor::codec::DagCborCodec;
486
487        let public_key = crate::crypto::SecretKey::generate().public();
488        let principal = Principal {
489            role: PrincipalRole::Owner,
490            identity: public_key,
491        };
492
493        // Try to encode/decode just the Principal
494        let encoded = DagCborCodec::encode_to_vec(&principal).unwrap();
495        let decoded: Principal = DagCborCodec::decode_from_slice(&encoded).unwrap();
496
497        assert_eq!(principal, decoded);
498    }
499
500    #[test]
501    fn test_manifest_signing() {
502        let secret_key = SecretKey::generate();
503        let mut manifest = create_test_manifest();
504
505        // Initially unsigned
506        assert!(!manifest.is_signed());
507        assert!(manifest.author().is_none());
508        assert!(manifest.signature().is_none());
509
510        // Sign the manifest
511        manifest.sign(&secret_key).unwrap();
512
513        // Now should be signed
514        assert!(manifest.is_signed());
515        assert_eq!(manifest.author(), Some(&secret_key.public()));
516        assert!(manifest.signature().is_some());
517
518        // Verify the signature
519        assert!(manifest.verify_signature().unwrap());
520    }
521
522    #[test]
523    fn test_manifest_tamper_detection() {
524        let secret_key = SecretKey::generate();
525        let mut manifest = create_test_manifest();
526
527        // Sign the manifest
528        manifest.sign(&secret_key).unwrap();
529        assert!(manifest.verify_signature().unwrap());
530
531        // Tamper with the manifest - change the height
532        manifest.set_height(999);
533
534        // Verification should now fail
535        let result = manifest.verify_signature();
536        assert!(result.is_err());
537    }
538
539    #[test]
540    fn test_unsigned_manifest_backwards_compatibility() {
541        use ipld_core::codec::Codec;
542        use serde_ipld_dagcbor::codec::DagCborCodec;
543
544        // Create a manifest without signing (simulates old unsigned manifest)
545        let manifest = create_test_manifest();
546        assert!(!manifest.is_signed());
547
548        // Serialize it
549        let encoded = DagCborCodec::encode_to_vec(&manifest).unwrap();
550
551        // Deserialize it back
552        let decoded: Manifest = DagCborCodec::decode_from_slice(&encoded).unwrap();
553
554        // Should still work without author/signature
555        assert!(!decoded.is_signed());
556        assert!(decoded.author().is_none());
557        assert!(decoded.signature().is_none());
558
559        // verify_signature should return Ok(false) for unsigned manifests
560        assert!(!decoded.verify_signature().unwrap());
561    }
562
563    #[test]
564    fn test_manifest_wrong_key_verification() {
565        let secret_key1 = SecretKey::generate();
566        let secret_key2 = SecretKey::generate();
567        let mut manifest = create_test_manifest();
568
569        // Sign with key 1
570        manifest.sign(&secret_key1).unwrap();
571        assert!(manifest.verify_signature().unwrap());
572
573        // Manually change the author to key 2's public key (simulating a forgery attempt)
574        // We need to access the author field directly for this test
575        // Since we can't modify it directly, we'll just verify that the current
576        // signature was made with key 1, not key 2
577        assert_eq!(manifest.author(), Some(&secret_key1.public()));
578        assert_ne!(manifest.author(), Some(&secret_key2.public()));
579    }
580}