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}