Skip to main content

opentdf_protocol/
manifest.rs

1//! TDF Manifest structures
2//!
3//! This module contains the data structures for TDF manifests, including:
4//! - Payload information
5//! - Encryption configuration
6//! - Key access objects
7//! - Integrity information (segments and root signature)
8//!
9//! Note: Cryptographic operations (HMAC, policy binding generation) are in the crypto crate.
10
11use serde::{Deserialize, Serialize};
12
13/// TDF manifest structure
14#[derive(Debug, Serialize, Deserialize)]
15pub struct TdfManifest {
16    pub payload: Payload,
17    #[serde(rename = "encryptionInformation")]
18    pub encryption_information: EncryptionInformation,
19    #[serde(rename = "schemaVersion", skip_serializing_if = "Option::is_none")]
20    pub schema_version: Option<String>,
21}
22
23/// Payload reference in TDF manifest
24#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct Payload {
26    #[serde(rename = "type")]
27    pub payload_type: String,
28    pub url: String,
29    pub protocol: String,
30    #[serde(rename = "isEncrypted")]
31    pub is_encrypted: bool,
32    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
33    pub mime_type: Option<String>,
34    #[serde(rename = "tdf_spec_version", skip_serializing_if = "Option::is_none")]
35    pub tdf_spec_version: Option<String>,
36}
37
38impl Default for Payload {
39    fn default() -> Self {
40        Self {
41            payload_type: "reference".to_string(),
42            url: "0.payload".to_string(),
43            protocol: "zip".to_string(),
44            is_encrypted: true,
45            mime_type: Some("application/octet-stream".to_string()),
46            // Match go/Swift: version lives on root `schemaVersion`, not payload.
47            // Emitting payload.tdf_spec_version breaks strict readers (e.g. otdf-python
48            // ManifestPayload, which has no such field).
49            tdf_spec_version: None,
50        }
51    }
52}
53
54/// Encryption information in TDF manifest
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct EncryptionInformation {
57    #[serde(rename = "type")]
58    pub encryption_type: String,
59    #[serde(rename = "keyAccess")]
60    pub key_access: Vec<KeyAccess>,
61    pub method: EncryptionMethod,
62    #[serde(rename = "integrityInformation")]
63    pub integrity_information: IntegrityInformation,
64    pub policy: String,
65}
66
67/// Policy binding structure
68#[derive(Debug, Serialize, Deserialize, Clone)]
69pub struct PolicyBinding {
70    pub alg: String,
71    pub hash: String,
72}
73
74/// Key access type constants for TDF3
75pub mod key_access_type {
76    /// Standard RSA-wrapped key (RSA-OAEP)
77    pub const WRAPPED: &str = "wrapped";
78    /// EC-wrapped key (ECIES: ECDH + HKDF + AES-GCM)
79    pub const EC_WRAPPED: &str = "ec-wrapped";
80    /// Remote key access (key stored on KAS)
81    pub const REMOTE: &str = "remote";
82    /// Remote wrapped key access
83    pub const REMOTE_WRAPPED: &str = "remoteWrapped";
84}
85
86/// Key access object in manifest
87#[derive(Debug, Serialize, Deserialize, Clone)]
88pub struct KeyAccess {
89    #[serde(rename = "type")]
90    pub access_type: String,
91    pub url: String,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub kid: Option<String>,
94    pub protocol: String,
95    #[serde(rename = "wrappedKey")]
96    pub wrapped_key: String,
97    #[serde(rename = "policyBinding")]
98    pub policy_binding: PolicyBinding,
99    #[serde(rename = "encryptedMetadata", skip_serializing_if = "Option::is_none")]
100    pub encrypted_metadata: Option<String>,
101    #[serde(rename = "schemaVersion", skip_serializing_if = "Option::is_none")]
102    pub schema_version: Option<String>,
103    /// Ephemeral public key for EC-wrapped keys (PEM format)
104    #[serde(rename = "ephemeralPublicKey", skip_serializing_if = "Option::is_none")]
105    pub ephemeral_public_key: Option<String>,
106}
107
108impl KeyAccess {
109    /// Creates a new KeyAccess object with default values (RSA-wrapped)
110    pub fn new(url: String) -> Self {
111        KeyAccess {
112            access_type: key_access_type::WRAPPED.to_string(),
113            url,
114            kid: None,
115            protocol: "kas".to_string(),
116            wrapped_key: String::new(),
117            policy_binding: PolicyBinding {
118                alg: "HS256".to_string(),
119                hash: String::new(),
120            },
121            encrypted_metadata: None,
122            schema_version: Some("1.0".to_string()),
123            ephemeral_public_key: None,
124        }
125    }
126
127    /// Creates a new KeyAccess object for EC-wrapped keys
128    pub fn new_ec_wrapped(url: String) -> Self {
129        KeyAccess {
130            access_type: key_access_type::EC_WRAPPED.to_string(),
131            url,
132            kid: None,
133            protocol: "kas".to_string(),
134            wrapped_key: String::new(),
135            policy_binding: PolicyBinding {
136                alg: "HS256".to_string(),
137                hash: String::new(),
138            },
139            encrypted_metadata: None,
140            schema_version: Some("1.0".to_string()),
141            ephemeral_public_key: None,
142        }
143    }
144
145    /// Check if this key access uses EC wrapping
146    pub fn is_ec_wrapped(&self) -> bool {
147        self.access_type == key_access_type::EC_WRAPPED
148    }
149
150    /// Check if this key access uses RSA wrapping
151    pub fn is_rsa_wrapped(&self) -> bool {
152        self.access_type == key_access_type::WRAPPED
153    }
154
155    /// Set the ephemeral public key (for EC-wrapped keys)
156    pub fn set_ephemeral_public_key(&mut self, pem: String) {
157        self.ephemeral_public_key = Some(pem);
158    }
159}
160
161/// Encryption method configuration
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct EncryptionMethod {
164    pub algorithm: String,
165    #[serde(rename = "isStreamable")]
166    pub is_streamable: bool,
167    pub iv: String,
168}
169
170impl Default for EncryptionMethod {
171    fn default() -> Self {
172        Self {
173            algorithm: "AES-256-GCM".to_string(),
174            is_streamable: true,
175            iv: String::new(),
176        }
177    }
178}
179
180/// Integrity information including segments and root signature
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct IntegrityInformation {
183    #[serde(rename = "rootSignature")]
184    pub root_signature: RootSignature,
185    #[serde(rename = "segmentHashAlg")]
186    pub segment_hash_alg: String,
187    pub segments: Vec<Segment>,
188    #[serde(rename = "segmentSizeDefault")]
189    pub segment_size_default: u64,
190    #[serde(rename = "encryptedSegmentSizeDefault")]
191    pub encrypted_segment_size_default: u64,
192}
193
194impl Default for IntegrityInformation {
195    fn default() -> Self {
196        Self {
197            root_signature: RootSignature::default(),
198            segment_hash_alg: "GMAC".to_string(),
199            segments: Vec::new(),
200            segment_size_default: 1024 * 1024,                // 1MB
201            encrypted_segment_size_default: 1024 * 1024 + 28, // +IV+tag
202        }
203    }
204}
205
206/// Root signature for integrity verification
207#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct RootSignature {
209    pub alg: String,
210    pub sig: String,
211}
212
213impl Default for RootSignature {
214    fn default() -> Self {
215        Self {
216            alg: "HS256".to_string(),
217            sig: String::new(),
218        }
219    }
220}
221
222/// Segment information
223#[derive(Debug, Clone, Serialize, Deserialize)]
224pub struct Segment {
225    pub hash: String,
226    #[serde(rename = "segmentSize", skip_serializing_if = "Option::is_none")]
227    pub segment_size: Option<u64>,
228    #[serde(
229        rename = "encryptedSegmentSize",
230        skip_serializing_if = "Option::is_none"
231    )]
232    pub encrypted_segment_size: Option<u64>,
233}
234
235impl TdfManifest {
236    /// Creates a new TDF manifest with basic structure
237    pub fn new(payload_url: String, kas_url: String) -> Self {
238        TdfManifest {
239            payload: Payload {
240                payload_type: "reference".to_string(),
241                url: payload_url,
242                protocol: "zip".to_string(),
243                is_encrypted: true,
244                mime_type: Some("application/octet-stream".to_string()),
245                // Omit by default — go/Swift only set root schemaVersion.
246                tdf_spec_version: None,
247            },
248            encryption_information: EncryptionInformation {
249                encryption_type: "split".to_string(),
250                key_access: vec![KeyAccess::new(kas_url)],
251                method: EncryptionMethod {
252                    algorithm: "AES-256-GCM".to_string(),
253                    is_streamable: true,
254                    iv: String::new(),
255                },
256                integrity_information: IntegrityInformation {
257                    root_signature: RootSignature {
258                        alg: "HS256".to_string(),
259                        sig: String::new(),
260                    },
261                    segment_hash_alg: "GMAC".to_string(),
262                    segments: Vec::new(),
263                    segment_size_default: 1024 * 1024, // 1MB default
264                    encrypted_segment_size_default: 1024 * 1024 + 28, // +IV+tag
265                },
266                policy: String::new(),
267            },
268            schema_version: Some("3.0.0".to_string()),
269        }
270    }
271
272    /// Set the policy for the manifest using a raw string
273    pub fn set_policy_raw(&mut self, policy: &str) {
274        use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
275        self.encryption_information.policy = BASE64.encode(policy);
276    }
277
278    /// Get the decoded policy from the manifest as a raw string
279    pub fn get_policy_raw(&self) -> Result<String, base64::DecodeError> {
280        use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
281        let bytes = BASE64.decode(&self.encryption_information.policy)?;
282        String::from_utf8(bytes)
283            .map_err(|err| base64::DecodeError::InvalidByte(err.utf8_error().valid_up_to(), 0))
284    }
285
286    /// Add a segment to the manifest
287    pub fn add_segment(
288        &mut self,
289        hash: String,
290        segment_size: Option<u64>,
291        encrypted_segment_size: Option<u64>,
292    ) {
293        self.encryption_information
294            .integrity_information
295            .segments
296            .push(Segment {
297                hash,
298                segment_size,
299                encrypted_segment_size,
300            });
301    }
302
303    /// Serialize to JSON
304    pub fn to_json(&self) -> Result<String, serde_json::Error> {
305        serde_json::to_string(self)
306    }
307
308    /// Deserialize from JSON
309    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
310        serde_json::from_str(json)
311    }
312}
313
314impl KeyAccess {
315    /// Set encrypted metadata
316    pub fn set_encrypted_metadata(&mut self, metadata: &str) {
317        use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
318        self.encrypted_metadata = Some(BASE64.encode(metadata));
319    }
320
321    /// Clear encrypted metadata
322    pub fn clear_encrypted_metadata(&mut self) {
323        self.encrypted_metadata = None;
324    }
325}