1use serde::{Deserialize, Serialize};
12
13#[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 #[serde(rename = "tdf_spec_version", skip_serializing_if = "Option::is_none")]
24 pub tdf_spec_version: Option<String>,
25 #[serde(skip_serializing_if = "Option::is_none")]
27 pub gguf: Option<crate::GgufIndex>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct Payload {
33 #[serde(rename = "type")]
34 pub payload_type: String,
35 pub url: String,
36 pub protocol: String,
37 #[serde(rename = "isEncrypted")]
38 pub is_encrypted: bool,
39 #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
40 pub mime_type: Option<String>,
41 #[serde(rename = "tdf_spec_version", skip_serializing_if = "Option::is_none")]
42 pub tdf_spec_version: Option<String>,
43}
44
45impl Default for Payload {
46 fn default() -> Self {
47 Self {
48 payload_type: "reference".to_string(),
49 url: "0.payload".to_string(),
50 protocol: "zip".to_string(),
51 is_encrypted: true,
52 mime_type: Some("application/octet-stream".to_string()),
53 tdf_spec_version: None,
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct EncryptionInformation {
64 #[serde(rename = "type")]
65 pub encryption_type: String,
66 #[serde(rename = "keyAccess")]
67 pub key_access: Vec<KeyAccess>,
68 pub method: EncryptionMethod,
69 #[serde(rename = "integrityInformation")]
70 pub integrity_information: IntegrityInformation,
71 pub policy: String,
72}
73
74#[derive(Debug, Serialize, Deserialize, Clone)]
76pub struct PolicyBinding {
77 pub alg: String,
78 pub hash: String,
79}
80
81pub mod key_access_type {
83 pub const WRAPPED: &str = "wrapped";
85 pub const EC_WRAPPED: &str = "ec-wrapped";
87 pub const REMOTE: &str = "remote";
89 pub const REMOTE_WRAPPED: &str = "remoteWrapped";
91}
92
93#[derive(Debug, Serialize, Deserialize, Clone)]
95pub struct KeyAccess {
96 #[serde(rename = "type")]
97 pub access_type: String,
98 pub url: String,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub kid: Option<String>,
101 pub protocol: String,
102 #[serde(rename = "wrappedKey")]
103 pub wrapped_key: String,
104 #[serde(rename = "policyBinding")]
105 pub policy_binding: PolicyBinding,
106 #[serde(rename = "encryptedMetadata", skip_serializing_if = "Option::is_none")]
107 pub encrypted_metadata: Option<String>,
108 #[serde(rename = "schemaVersion", skip_serializing_if = "Option::is_none")]
109 pub schema_version: Option<String>,
110 #[serde(rename = "ephemeralPublicKey", skip_serializing_if = "Option::is_none")]
112 pub ephemeral_public_key: Option<String>,
113}
114
115impl KeyAccess {
116 pub fn new(url: String) -> Self {
118 KeyAccess {
119 access_type: key_access_type::WRAPPED.to_string(),
120 url,
121 kid: None,
122 protocol: "kas".to_string(),
123 wrapped_key: String::new(),
124 policy_binding: PolicyBinding {
125 alg: "HS256".to_string(),
126 hash: String::new(),
127 },
128 encrypted_metadata: None,
129 schema_version: Some("1.0".to_string()),
130 ephemeral_public_key: None,
131 }
132 }
133
134 pub fn new_ec_wrapped(url: String) -> Self {
136 KeyAccess {
137 access_type: key_access_type::EC_WRAPPED.to_string(),
138 url,
139 kid: None,
140 protocol: "kas".to_string(),
141 wrapped_key: String::new(),
142 policy_binding: PolicyBinding {
143 alg: "HS256".to_string(),
144 hash: String::new(),
145 },
146 encrypted_metadata: None,
147 schema_version: Some("1.0".to_string()),
148 ephemeral_public_key: None,
149 }
150 }
151
152 pub fn is_ec_wrapped(&self) -> bool {
154 self.access_type == key_access_type::EC_WRAPPED
155 }
156
157 pub fn is_rsa_wrapped(&self) -> bool {
159 self.access_type == key_access_type::WRAPPED
160 }
161
162 pub fn set_ephemeral_public_key(&mut self, pem: String) {
164 self.ephemeral_public_key = Some(pem);
165 }
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct EncryptionMethod {
171 pub algorithm: String,
172 #[serde(rename = "isStreamable")]
173 pub is_streamable: bool,
174 pub iv: String,
175}
176
177impl Default for EncryptionMethod {
178 fn default() -> Self {
179 Self {
180 algorithm: "AES-256-GCM".to_string(),
181 is_streamable: true,
182 iv: String::new(),
183 }
184 }
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct IntegrityInformation {
190 #[serde(rename = "rootSignature")]
191 pub root_signature: RootSignature,
192 #[serde(rename = "segmentHashAlg")]
193 pub segment_hash_alg: String,
194 pub segments: Vec<Segment>,
195 #[serde(rename = "segmentSizeDefault")]
196 pub segment_size_default: u64,
197 #[serde(rename = "encryptedSegmentSizeDefault")]
198 pub encrypted_segment_size_default: u64,
199}
200
201impl Default for IntegrityInformation {
202 fn default() -> Self {
203 Self {
204 root_signature: RootSignature::default(),
205 segment_hash_alg: "GMAC".to_string(),
206 segments: Vec::new(),
207 segment_size_default: 1024 * 1024, encrypted_segment_size_default: 1024 * 1024 + 28, }
210 }
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct RootSignature {
216 pub alg: String,
217 pub sig: String,
218}
219
220impl Default for RootSignature {
221 fn default() -> Self {
222 Self {
223 alg: "HS256".to_string(),
224 sig: String::new(),
225 }
226 }
227}
228
229#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct Segment {
232 pub hash: String,
233 #[serde(rename = "segmentSize", skip_serializing_if = "Option::is_none")]
234 pub segment_size: Option<u64>,
235 #[serde(
236 rename = "encryptedSegmentSize",
237 skip_serializing_if = "Option::is_none"
238 )]
239 pub encrypted_segment_size: Option<u64>,
240}
241
242impl TdfManifest {
243 pub fn new(payload_url: String, kas_url: String) -> Self {
245 TdfManifest {
246 payload: Payload {
247 payload_type: "reference".to_string(),
248 url: payload_url,
249 protocol: "zip".to_string(),
250 is_encrypted: true,
251 mime_type: Some("application/octet-stream".to_string()),
252 tdf_spec_version: None,
254 },
255 encryption_information: EncryptionInformation {
256 encryption_type: "split".to_string(),
257 key_access: vec![KeyAccess::new(kas_url)],
258 method: EncryptionMethod {
259 algorithm: "AES-256-GCM".to_string(),
260 is_streamable: true,
261 iv: String::new(),
262 },
263 integrity_information: IntegrityInformation {
264 root_signature: RootSignature {
265 alg: "HS256".to_string(),
266 sig: String::new(),
267 },
268 segment_hash_alg: "GMAC".to_string(),
269 segments: Vec::new(),
270 segment_size_default: 1024 * 1024, encrypted_segment_size_default: 1024 * 1024 + 28, },
273 policy: String::new(),
274 },
275 schema_version: Some("3.0.0".to_string()),
276 tdf_spec_version: None,
277 gguf: None,
278 }
279 }
280
281 pub fn set_policy_raw(&mut self, policy: &str) {
283 use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
284 self.encryption_information.policy = BASE64.encode(policy);
285 }
286
287 pub fn get_policy_raw(&self) -> Result<String, base64::DecodeError> {
289 use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
290 let bytes = BASE64.decode(&self.encryption_information.policy)?;
291 String::from_utf8(bytes)
292 .map_err(|err| base64::DecodeError::InvalidByte(err.utf8_error().valid_up_to(), 0))
293 }
294
295 pub fn add_segment(
297 &mut self,
298 hash: String,
299 segment_size: Option<u64>,
300 encrypted_segment_size: Option<u64>,
301 ) {
302 self.encryption_information
303 .integrity_information
304 .segments
305 .push(Segment {
306 hash,
307 segment_size,
308 encrypted_segment_size,
309 });
310 }
311
312 pub fn to_json(&self) -> Result<String, serde_json::Error> {
314 serde_json::to_string(self)
315 }
316
317 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
319 serde_json::from_str(json)
320 }
321}
322
323impl KeyAccess {
324 pub fn set_encrypted_metadata(&mut self, metadata: &str) {
326 use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
327 self.encrypted_metadata = Some(BASE64.encode(metadata));
328 }
329
330 pub fn clear_encrypted_metadata(&mut self) {
332 self.encrypted_metadata = None;
333 }
334}
335
336#[cfg(test)]
337mod gguf_index_tests {
338 use super::*;
339
340 #[test]
341 fn gguf_index_round_trips_and_stays_absent_by_default() {
342 let plain = TdfManifest::new(
344 "0.payload".to_string(),
345 "https://kas.example.com".to_string(),
346 );
347 let value: serde_json::Value = serde_json::from_str(&plain.to_json().unwrap()).unwrap();
348 assert!(
349 value.get("gguf").is_none(),
350 "gguf must be absent when unset: {value}"
351 );
352 assert!(
353 value.get("tdf_spec_version").is_none(),
354 "tdf_spec_version must be absent when unset: {value}"
355 );
356
357 let mut m = TdfManifest::new("header".to_string(), "https://kas.example.com".to_string());
359 m.tdf_spec_version = Some("4.3.0".to_string());
360 m.gguf = Some(crate::GgufIndex {
361 profile: crate::GGUF_TDF_PROFILE_V1.to_string(),
362 alignment: 32,
363 header_bytes: 64,
364 virtual_size: 352,
365 max_segment: 128,
366 tensors: vec![crate::GgufTensor {
367 name: "token_embd.weight".to_string(),
368 offset: 64,
369 size: 256,
370 segments: [1, 3],
371 }],
372 segments: vec![crate::GgufSegment {
373 id: 0,
374 kind: crate::GgufSegmentKind::Header,
375 plain: 64,
376 entry: "header".to_string(),
377 }],
378 });
379
380 let json = m.to_json().unwrap();
381 let value: serde_json::Value = serde_json::from_str(&json).unwrap();
382 assert_eq!(value["tdf_spec_version"], "4.3.0");
383 assert_eq!(value["gguf"]["profile"], "gguf-tdf/1");
384 assert_eq!(value["gguf"]["headerBytes"], 64);
385 assert_eq!(value["gguf"]["maxSegment"], 128);
386 assert_eq!(value["gguf"]["segments"][0]["kind"], "header");
387 assert_eq!(value["gguf"]["tensors"][0]["segments"][1], 3);
388
389 let back = TdfManifest::from_json(&json).unwrap();
390 assert_eq!(back.gguf.as_ref().unwrap().virtual_size, 352);
391 assert_eq!(back.tdf_spec_version.as_deref(), Some("4.3.0"));
392 }
393}