Skip to main content

sdk_rust/
local.rs

1use std::collections::BTreeMap;
2
3use aes_gcm::{
4    Aes256Gcm, Nonce,
5    aead::{Aead, KeyInit},
6};
7use base64::{Engine, engine::general_purpose::STANDARD as BASE64_STANDARD};
8use ed25519_dalek::{
9    Signature as Ed25519Signature, Signer as _, SigningKey as Ed25519SigningKey, Verifier as _,
10    VerifyingKey as Ed25519VerifyingKey,
11};
12use rand::RngCore;
13use serde::{Deserialize, Serialize};
14use sha2::{Digest, Sha256};
15use zeroize::{Zeroize, ZeroizeOnDrop};
16
17use crate::{
18    Client, SdkError,
19    models::{
20        ArtifactProfile, EvidenceEventType, KeyAccessOperation, KeyTransportGuidance,
21        KeyTransportMode, ProtectionOperation, RequestContext, ResourceDescriptor,
22        SdkArtifactRegisterRequest, SdkArtifactRegisterResponse, SdkBootstrapResponse,
23        SdkEvidenceIngestRequest, SdkEvidenceIngestResponse, SdkKeyAccessPlanRequest,
24        SdkKeyAccessPlanResponse, SdkPolicyResolveRequest, SdkPolicyResolveResponse,
25        SdkProtectionPlanRequest, SdkProtectionPlanResponse, WorkloadDescriptor,
26    },
27};
28
29const LOCAL_ENVELOPE_KEY_REFERENCE: &str = "local-symmetric-key";
30const LOCAL_TDF_KEY_REFERENCE: &str = "local-tdf-key";
31const LOCAL_DETACHED_SIGNATURE_KEY_REFERENCE: &str = "local-detached-signature-key";
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct LocalProtectionRequest {
35    pub workload: WorkloadDescriptor,
36    pub resource: ResourceDescriptor,
37    pub preferred_artifact_profile: Option<ArtifactProfile>,
38    pub purpose: Option<String>,
39    #[serde(default)]
40    pub labels: Vec<String>,
41    #[serde(default)]
42    pub attributes: BTreeMap<String, String>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
46pub struct LocalAttributeEdit {
47    #[serde(default)]
48    pub set: BTreeMap<String, String>,
49    #[serde(default)]
50    pub remove: Vec<String>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
54pub struct LocalContentBinding {
55    pub tenant_id: String,
56    pub content_digest: String,
57    pub content_size_bytes: u64,
58    pub raw_cid: String,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62pub struct LocalArtifactBinding {
63    #[serde(default = "default_binding_version")]
64    pub version: u8,
65    pub tenant_id: String,
66    pub raw_cid: String,
67    pub content_digest: String,
68    pub content_size_bytes: u64,
69    pub workload: WorkloadDescriptor,
70    pub resource: ResourceDescriptor,
71    pub purpose: Option<String>,
72    #[serde(default)]
73    pub labels: Vec<String>,
74    #[serde(default)]
75    pub attributes: BTreeMap<String, String>,
76    #[serde(default)]
77    pub binding_targets: Vec<String>,
78    #[serde(default)]
79    pub binding_hash: String,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
83pub struct PreparedLocalProtection {
84    pub caller: RequestContext,
85    pub content_binding: LocalContentBinding,
86    pub artifact_binding: LocalArtifactBinding,
87    pub bootstrap: SdkBootstrapResponse,
88    pub policy_resolution: SdkPolicyResolveResponse,
89    pub protection_plan: SdkProtectionPlanResponse,
90}
91
92impl PreparedLocalProtection {
93    pub fn resolved_artifact_profile(&self) -> ArtifactProfile {
94        self.protection_plan.execution.artifact_profile
95    }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
99pub struct LocalSymmetricKey([u8; 32]);
100
101impl LocalSymmetricKey {
102    pub fn new(bytes: [u8; 32]) -> Self {
103        Self(bytes)
104    }
105
106    fn as_bytes(&self) -> &[u8; 32] {
107        &self.0
108    }
109}
110
111impl From<[u8; 32]> for LocalSymmetricKey {
112    fn from(value: [u8; 32]) -> Self {
113        Self::new(value)
114    }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
118pub struct ManagedSymmetricKeyReference {
119    pub key_reference: String,
120    #[serde(default, skip_serializing_if = "Option::is_none")]
121    pub provider_name: Option<String>,
122}
123
124impl ManagedSymmetricKeyReference {
125    pub fn new(key_reference: impl Into<String>) -> Self {
126        Self {
127            key_reference: key_reference.into(),
128            provider_name: None,
129        }
130    }
131
132    pub fn with_provider(
133        provider_name: impl Into<String>,
134        key_reference: impl Into<String>,
135    ) -> Self {
136        Self {
137            key_reference: key_reference.into(),
138            provider_name: Some(provider_name.into()),
139        }
140    }
141
142    pub fn key_reference(&self) -> &str {
143        &self.key_reference
144    }
145
146    pub fn provider_name(&self) -> Option<&str> {
147        self.provider_name.as_deref()
148    }
149}
150
151#[derive(Debug, Clone, PartialEq, Eq)]
152pub enum LocalSymmetricKeySource {
153    Inline(LocalSymmetricKey),
154    ManagedReference(ManagedSymmetricKeyReference),
155}
156
157impl LocalSymmetricKeySource {
158    pub fn inline(key: impl Into<LocalSymmetricKey>) -> Self {
159        Self::Inline(key.into())
160    }
161
162    pub fn managed_reference(key_reference: impl Into<String>) -> Self {
163        Self::ManagedReference(ManagedSymmetricKeyReference::new(key_reference))
164    }
165
166    pub fn managed_reference_with_provider(
167        provider_name: impl Into<String>,
168        key_reference: impl Into<String>,
169    ) -> Self {
170        Self::ManagedReference(ManagedSymmetricKeyReference::with_provider(
171            provider_name,
172            key_reference,
173        ))
174    }
175
176    pub fn key_reference<'a>(&'a self, default_key_reference: &'a str) -> &'a str {
177        match self {
178            Self::Inline(_) => default_key_reference,
179            Self::ManagedReference(key_reference) => key_reference.key_reference(),
180        }
181    }
182
183    pub fn provider_name(&self) -> Option<&str> {
184        match self {
185            Self::Inline(_) => None,
186            Self::ManagedReference(key_reference) => key_reference.provider_name(),
187        }
188    }
189}
190
191impl From<LocalSymmetricKey> for LocalSymmetricKeySource {
192    fn from(value: LocalSymmetricKey) -> Self {
193        Self::Inline(value)
194    }
195}
196
197impl From<[u8; 32]> for LocalSymmetricKeySource {
198    fn from(value: [u8; 32]) -> Self {
199        Self::Inline(LocalSymmetricKey::from(value))
200    }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq, Zeroize, ZeroizeOnDrop)]
204pub struct LocalSigningKey([u8; 32]);
205
206impl LocalSigningKey {
207    pub fn new(bytes: [u8; 32]) -> Self {
208        Self(bytes)
209    }
210
211    fn as_bytes(&self) -> &[u8; 32] {
212        &self.0
213    }
214
215    pub fn verifying_key(&self) -> LocalVerifyingKey {
216        let signing_key = Ed25519SigningKey::from_bytes(self.as_bytes());
217        LocalVerifyingKey::from(signing_key.verifying_key().to_bytes())
218    }
219}
220
221impl From<[u8; 32]> for LocalSigningKey {
222    fn from(value: [u8; 32]) -> Self {
223        Self::new(value)
224    }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct LocalVerifyingKey([u8; 32]);
229
230impl LocalVerifyingKey {
231    pub fn new(bytes: [u8; 32]) -> Self {
232        Self(bytes)
233    }
234
235    fn as_bytes(&self) -> &[u8; 32] {
236        &self.0
237    }
238}
239
240impl From<[u8; 32]> for LocalVerifyingKey {
241    fn from(value: [u8; 32]) -> Self {
242        Self::new(value)
243    }
244}
245
246#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
247#[serde(rename_all = "snake_case")]
248pub enum LocalEnvelopeAlgorithm {
249    Aes256Gcm,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
253pub struct LocalEnvelopeArtifact {
254    pub version: u8,
255    pub artifact_profile: ArtifactProfile,
256    pub algorithm: LocalEnvelopeAlgorithm,
257    pub tenant_id: String,
258    pub raw_cid: String,
259    pub content_digest: String,
260    pub content_size_bytes: u64,
261    pub workload: WorkloadDescriptor,
262    pub resource: ResourceDescriptor,
263    pub purpose: Option<String>,
264    pub labels: Vec<String>,
265    pub attributes: BTreeMap<String, String>,
266    #[serde(default)]
267    pub binding_targets: Vec<String>,
268    #[serde(default)]
269    pub binding_hash: String,
270    pub nonce_b64: String,
271    pub aad_hash: String,
272    pub ciphertext_b64: String,
273}
274
275#[derive(Debug, Clone, PartialEq, Eq)]
276pub struct ProtectedEnvelopeArtifact {
277    pub envelope: LocalEnvelopeArtifact,
278    pub artifact_bytes: Vec<u8>,
279    pub artifact_digest: String,
280}
281
282#[derive(Debug, Clone, PartialEq, Eq)]
283pub struct EnvelopeProtectionResult {
284    pub prepared: PreparedLocalProtection,
285    pub key_access_plan: SdkKeyAccessPlanResponse,
286    pub artifact: ProtectedEnvelopeArtifact,
287    pub artifact_registration: SdkArtifactRegisterResponse,
288    pub evidence: SdkEvidenceIngestResponse,
289}
290
291#[derive(Debug, Clone, PartialEq, Eq)]
292pub struct EnvelopeAccessResult {
293    pub artifact: LocalEnvelopeArtifact,
294    pub artifact_digest: String,
295    pub policy_resolution: SdkPolicyResolveResponse,
296    pub key_access_plan: SdkKeyAccessPlanResponse,
297    pub plaintext: Vec<u8>,
298    pub evidence: SdkEvidenceIngestResponse,
299}
300
301#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
302#[serde(rename_all = "snake_case")]
303pub enum LocalTdfAlgorithm {
304    Aes256Gcm,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
308pub struct LocalTdfManifest {
309    pub workload: WorkloadDescriptor,
310    pub resource: ResourceDescriptor,
311    pub purpose: Option<String>,
312    #[serde(default)]
313    pub labels: Vec<String>,
314    #[serde(default)]
315    pub attributes: BTreeMap<String, String>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
319pub struct LocalTdfArtifact {
320    pub version: u8,
321    #[serde(default = "default_meta_version")]
322    pub meta_version: u64,
323    pub artifact_profile: ArtifactProfile,
324    pub algorithm: LocalTdfAlgorithm,
325    pub tenant_id: String,
326    pub raw_cid: String,
327    pub content_digest: String,
328    pub content_size_bytes: u64,
329    pub manifest_digest: String,
330    #[serde(default)]
331    pub binding_targets: Vec<String>,
332    #[serde(default)]
333    pub binding_hash: String,
334    #[serde(default, skip_serializing_if = "Option::is_none")]
335    pub policy_context: Option<LocalTdfManifest>,
336    pub manifest_nonce_b64: String,
337    pub manifest_ciphertext_b64: String,
338    pub payload_nonce_b64: String,
339    pub payload_ciphertext_b64: String,
340    pub aad_hash: String,
341}
342
343#[derive(Debug, Clone, PartialEq, Eq)]
344pub struct ProtectedTdfArtifact {
345    pub tdf: LocalTdfArtifact,
346    pub artifact_bytes: Vec<u8>,
347    pub artifact_digest: String,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq)]
351pub struct TdfProtectionResult {
352    pub prepared: PreparedLocalProtection,
353    pub key_access_plan: SdkKeyAccessPlanResponse,
354    pub artifact: ProtectedTdfArtifact,
355    pub artifact_registration: SdkArtifactRegisterResponse,
356    pub evidence: SdkEvidenceIngestResponse,
357}
358
359#[derive(Debug, Clone, PartialEq, Eq)]
360pub struct TdfAccessResult {
361    pub artifact: LocalTdfArtifact,
362    pub manifest: LocalTdfManifest,
363    pub artifact_digest: String,
364    pub policy_resolution: SdkPolicyResolveResponse,
365    pub key_access_plan: SdkKeyAccessPlanResponse,
366    pub plaintext: Vec<u8>,
367    pub evidence: SdkEvidenceIngestResponse,
368}
369
370#[derive(Debug, Clone, PartialEq, Eq)]
371pub struct EnvelopeRewrapResult {
372    pub content_binding: LocalContentBinding,
373    pub policy_resolution: SdkPolicyResolveResponse,
374    pub protection_plan: SdkProtectionPlanResponse,
375    pub key_access_plan: SdkKeyAccessPlanResponse,
376    pub original_artifact_digest: String,
377    pub artifact: ProtectedEnvelopeArtifact,
378    pub artifact_registration: SdkArtifactRegisterResponse,
379    pub evidence: SdkEvidenceIngestResponse,
380}
381
382#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct TdfRewrapResult {
384    pub content_binding: LocalContentBinding,
385    pub manifest: LocalTdfManifest,
386    pub policy_resolution: SdkPolicyResolveResponse,
387    pub protection_plan: SdkProtectionPlanResponse,
388    pub key_access_plan: SdkKeyAccessPlanResponse,
389    pub original_artifact_digest: String,
390    pub artifact: ProtectedTdfArtifact,
391    pub artifact_registration: SdkArtifactRegisterResponse,
392    pub evidence: SdkEvidenceIngestResponse,
393}
394
395#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
396#[serde(rename_all = "snake_case")]
397pub enum LocalDetachedSignatureAlgorithm {
398    Ed25519,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
402pub struct LocalDetachedSignatureArtifact {
403    pub version: u8,
404    pub artifact_profile: ArtifactProfile,
405    pub algorithm: LocalDetachedSignatureAlgorithm,
406    pub tenant_id: String,
407    pub raw_cid: String,
408    pub content_digest: String,
409    pub content_size_bytes: u64,
410    pub workload: WorkloadDescriptor,
411    pub resource: ResourceDescriptor,
412    pub purpose: Option<String>,
413    #[serde(default)]
414    pub labels: Vec<String>,
415    #[serde(default)]
416    pub attributes: BTreeMap<String, String>,
417    #[serde(default)]
418    pub binding_targets: Vec<String>,
419    pub signer_key_id: String,
420    pub signer_public_key_b64: String,
421    #[serde(default)]
422    pub binding_hash: String,
423    pub signature_b64: String,
424}
425
426#[derive(Debug, Clone, PartialEq, Eq)]
427pub struct ProtectedDetachedSignatureArtifact {
428    pub detached_signature: LocalDetachedSignatureArtifact,
429    pub artifact_bytes: Vec<u8>,
430    pub artifact_digest: String,
431}
432
433#[derive(Debug, Clone, PartialEq, Eq)]
434pub struct DetachedSignatureSignResult {
435    pub prepared: PreparedLocalProtection,
436    pub key_access_plan: SdkKeyAccessPlanResponse,
437    pub artifact: ProtectedDetachedSignatureArtifact,
438    pub artifact_registration: SdkArtifactRegisterResponse,
439    pub evidence: SdkEvidenceIngestResponse,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq)]
443pub struct DetachedSignatureVerifyResult {
444    pub artifact: LocalDetachedSignatureArtifact,
445    pub artifact_digest: String,
446    pub policy_resolution: SdkPolicyResolveResponse,
447    pub key_access_plan: SdkKeyAccessPlanResponse,
448    pub content_binding: LocalContentBinding,
449    pub evidence: SdkEvidenceIngestResponse,
450}
451
452#[derive(Debug, Clone, Serialize)]
453struct EnvelopeAadBinding<'a> {
454    tenant_id: &'a str,
455    raw_cid: &'a str,
456    content_digest: &'a str,
457    artifact_profile: ArtifactProfile,
458    workload: &'a WorkloadDescriptor,
459    resource: &'a ResourceDescriptor,
460    purpose: &'a Option<String>,
461    labels: &'a [String],
462    attributes: &'a BTreeMap<String, String>,
463    binding_targets: &'a [String],
464    binding_hash: &'a str,
465}
466
467#[derive(Debug, Clone, Serialize)]
468struct TdfAadBinding<'a> {
469    tenant_id: &'a str,
470    raw_cid: &'a str,
471    content_digest: &'a str,
472    content_size_bytes: u64,
473    manifest_digest: &'a str,
474    artifact_profile: ArtifactProfile,
475    binding_hash: &'a str,
476}
477
478#[derive(Debug, Clone, Serialize)]
479struct LocalArtifactBindingPayload<'a> {
480    version: u8,
481    tenant_id: &'a str,
482    raw_cid: &'a str,
483    content_digest: &'a str,
484    content_size_bytes: u64,
485    workload: &'a WorkloadDescriptor,
486    resource: &'a ResourceDescriptor,
487    purpose: &'a Option<String>,
488    labels: &'a [String],
489    attributes: &'a BTreeMap<String, String>,
490    binding_targets: &'a [String],
491}
492
493impl Client {
494    pub fn prepare_local_protection(
495        &self,
496        content: &[u8],
497        request: LocalProtectionRequest,
498    ) -> Result<PreparedLocalProtection, SdkError> {
499        let digest = sha256_prefixed(content);
500        let content_size_bytes = u64::try_from(content.len()).map_err(|_| {
501            SdkError::InvalidInput("content length exceeds supported u64 range".to_string())
502        })?;
503
504        let bootstrap = self.bootstrap()?;
505        ensure_bootstrap_supports_local_protection(&bootstrap, request.preferred_artifact_profile)?;
506
507        let policy_resolution = self.policy_resolve(&SdkPolicyResolveRequest {
508            operation: ProtectionOperation::Protect,
509            workload: request.workload.clone(),
510            resource: request.resource.clone(),
511            content_digest: Some(digest.clone()),
512            content_size_bytes: Some(content_size_bytes),
513            purpose: request.purpose.clone(),
514            labels: request.labels.clone(),
515            attributes: request.attributes.clone(),
516        })?;
517        ensure_policy_resolution_supports_local_protection(&policy_resolution)?;
518
519        let protection_plan = self.protection_plan(&SdkProtectionPlanRequest {
520            operation: ProtectionOperation::Protect,
521            workload: request.workload.clone(),
522            resource: request.resource.clone(),
523            preferred_artifact_profile: request.preferred_artifact_profile,
524            content_digest: Some(digest.clone()),
525            content_size_bytes: Some(content_size_bytes),
526            purpose: request.purpose.clone(),
527            labels: request.labels.clone(),
528            attributes: request.attributes.clone(),
529        })?;
530        ensure_protection_plan_supports_local_protection(&protection_plan)?;
531
532        let caller = protection_plan.caller.clone();
533        let content_binding = LocalContentBinding {
534            tenant_id: caller.tenant_id.clone(),
535            content_digest: digest.clone(),
536            content_size_bytes,
537            raw_cid: digest,
538        };
539        let artifact_binding = build_local_artifact_binding(
540            &content_binding,
541            &request,
542            &policy_resolution.handling.bind_policy_to,
543        )?;
544
545        Ok(PreparedLocalProtection {
546            caller,
547            content_binding,
548            artifact_binding,
549            bootstrap,
550            policy_resolution,
551            protection_plan,
552        })
553    }
554
555    pub fn generate_cid_binding(
556        &self,
557        content: &[u8],
558        request: LocalProtectionRequest,
559    ) -> Result<LocalArtifactBinding, SdkError> {
560        Ok(self
561            .prepare_local_protection(content, request)?
562            .artifact_binding)
563    }
564
565    pub fn protect_bytes_with_envelope(
566        &self,
567        key: &LocalSymmetricKey,
568        plaintext: &[u8],
569        request: LocalProtectionRequest,
570    ) -> Result<EnvelopeProtectionResult, SdkError> {
571        self.protect_bytes_with_envelope_using_key_source(
572            &LocalSymmetricKeySource::from(key.clone()),
573            plaintext,
574            request,
575        )
576    }
577
578    pub fn protect_bytes_with_envelope_using_key_source(
579        &self,
580        key_source: &LocalSymmetricKeySource,
581        plaintext: &[u8],
582        mut request: LocalProtectionRequest,
583    ) -> Result<EnvelopeProtectionResult, SdkError> {
584        request.preferred_artifact_profile = Some(ArtifactProfile::Envelope);
585        let prepared = self.prepare_local_protection(plaintext, request.clone())?;
586
587        let key_access_plan = self.key_access_plan(&SdkKeyAccessPlanRequest {
588            operation: KeyAccessOperation::Wrap,
589            workload: request.workload.clone(),
590            resource: request.resource.clone(),
591            artifact_profile: Some(ArtifactProfile::Envelope),
592            key_reference: Some(
593                key_source
594                    .key_reference(LOCAL_ENVELOPE_KEY_REFERENCE)
595                    .to_string(),
596            ),
597            content_digest: Some(prepared.content_binding.content_digest.clone()),
598            purpose: request.purpose.clone(),
599            labels: request.labels.clone(),
600            attributes: request.attributes.clone(),
601        })?;
602        ensure_key_access_plan_supports_local_crypto(
603            &key_access_plan,
604            KeyAccessOperation::Wrap,
605            ArtifactProfile::Envelope,
606        )?;
607
608        let resolved_key = resolve_symmetric_key_for_runtime(
609            self,
610            key_source,
611            key_access_plan.execution.key_transport.as_ref(),
612            LOCAL_ENVELOPE_KEY_REFERENCE,
613            "local envelope protection",
614        )?;
615
616        let envelope =
617            encrypt_envelope_artifact(&resolved_key.key, plaintext, &prepared, &request)?;
618        let artifact_bytes = serde_json::to_vec(&envelope).map_err(|error| {
619            SdkError::Serialization(format!(
620                "failed to serialize local envelope artifact: {error}"
621            ))
622        })?;
623        let artifact_digest = sha256_prefixed(&artifact_bytes);
624
625        let artifact_registration = self.artifact_register(&SdkArtifactRegisterRequest {
626            operation: ProtectionOperation::Protect,
627            workload: request.workload.clone(),
628            resource: request.resource.clone(),
629            artifact_profile: ArtifactProfile::Envelope,
630            artifact_digest: artifact_digest.clone(),
631            artifact_locator: None,
632            decision_id: None,
633            key_reference: Some(resolved_key.key_reference),
634            purpose: request.purpose.clone(),
635            labels: request.labels.clone(),
636            attributes: request.attributes.clone(),
637        })?;
638        ensure_artifact_registration_supports_local_only(&artifact_registration)?;
639
640        let evidence = self.evidence(&SdkEvidenceIngestRequest {
641            event_type: EvidenceEventType::Protect,
642            workload: request.workload,
643            resource: request.resource,
644            artifact_profile: Some(ArtifactProfile::Envelope),
645            artifact_digest: Some(artifact_digest.clone()),
646            decision_id: None,
647            outcome: Some("success".to_string()),
648            occurred_at: None,
649            purpose: request.purpose,
650            labels: request.labels,
651            attributes: request.attributes,
652        })?;
653        ensure_evidence_ingestion_supports_local_only(&evidence)?;
654
655        Ok(EnvelopeProtectionResult {
656            prepared,
657            key_access_plan,
658            artifact: ProtectedEnvelopeArtifact {
659                envelope,
660                artifact_bytes,
661                artifact_digest,
662            },
663            artifact_registration,
664            evidence,
665        })
666    }
667
668    pub fn access_bytes_with_envelope(
669        &self,
670        key: &LocalSymmetricKey,
671        artifact_bytes: &[u8],
672    ) -> Result<EnvelopeAccessResult, SdkError> {
673        self.access_bytes_with_envelope_using_key_source(
674            &LocalSymmetricKeySource::from(key.clone()),
675            artifact_bytes,
676        )
677    }
678
679    pub fn access_bytes_with_envelope_using_key_source(
680        &self,
681        key_source: &LocalSymmetricKeySource,
682        artifact_bytes: &[u8],
683    ) -> Result<EnvelopeAccessResult, SdkError> {
684        let artifact: LocalEnvelopeArtifact =
685            serde_json::from_slice(artifact_bytes).map_err(|error| {
686                SdkError::Serialization(format!(
687                    "failed to decode local envelope artifact: {error}"
688                ))
689            })?;
690        ensure_local_envelope_artifact_valid(&artifact)?;
691
692        let artifact_digest = sha256_prefixed(artifact_bytes);
693
694        let policy_resolution = self.policy_resolve(&SdkPolicyResolveRequest {
695            operation: ProtectionOperation::Access,
696            workload: artifact.workload.clone(),
697            resource: artifact.resource.clone(),
698            content_digest: Some(artifact.content_digest.clone()),
699            content_size_bytes: Some(artifact.content_size_bytes),
700            purpose: artifact.purpose.clone(),
701            labels: artifact.labels.clone(),
702            attributes: artifact.attributes.clone(),
703        })?;
704        ensure_access_policy_resolution_supports_local_access(&policy_resolution)?;
705
706        let key_access_plan = self.key_access_plan(&SdkKeyAccessPlanRequest {
707            operation: KeyAccessOperation::Unwrap,
708            workload: artifact.workload.clone(),
709            resource: artifact.resource.clone(),
710            artifact_profile: Some(ArtifactProfile::Envelope),
711            key_reference: Some(
712                key_source
713                    .key_reference(LOCAL_ENVELOPE_KEY_REFERENCE)
714                    .to_string(),
715            ),
716            content_digest: Some(artifact.content_digest.clone()),
717            purpose: artifact.purpose.clone(),
718            labels: artifact.labels.clone(),
719            attributes: artifact.attributes.clone(),
720        })?;
721        ensure_key_access_plan_supports_local_crypto(
722            &key_access_plan,
723            KeyAccessOperation::Unwrap,
724            ArtifactProfile::Envelope,
725        )?;
726
727        let resolved_key = resolve_symmetric_key_for_runtime(
728            self,
729            key_source,
730            key_access_plan.execution.key_transport.as_ref(),
731            LOCAL_ENVELOPE_KEY_REFERENCE,
732            "local envelope access",
733        )?;
734
735        let plaintext = decrypt_envelope_artifact(&resolved_key.key, &artifact)?;
736        let decrypted_digest = sha256_prefixed(&plaintext);
737        if decrypted_digest != artifact.content_digest {
738            return Err(SdkError::InvalidInput(
739                "local envelope decrypted bytes do not match the embedded content digest"
740                    .to_string(),
741            ));
742        }
743
744        let decrypted_size = u64::try_from(plaintext.len()).map_err(|_| {
745            SdkError::InvalidInput(
746                "decrypted content length exceeds supported u64 range".to_string(),
747            )
748        })?;
749        if decrypted_size != artifact.content_size_bytes {
750            return Err(SdkError::InvalidInput(
751                "local envelope decrypted bytes do not match the embedded content size".to_string(),
752            ));
753        }
754
755        let evidence = self.evidence(&SdkEvidenceIngestRequest {
756            event_type: EvidenceEventType::Access,
757            workload: artifact.workload.clone(),
758            resource: artifact.resource.clone(),
759            artifact_profile: Some(ArtifactProfile::Envelope),
760            artifact_digest: Some(artifact_digest.clone()),
761            decision_id: None,
762            outcome: Some("success".to_string()),
763            occurred_at: None,
764            purpose: artifact.purpose.clone(),
765            labels: artifact.labels.clone(),
766            attributes: artifact.attributes.clone(),
767        })?;
768        ensure_evidence_ingestion_supports_local_only(&evidence)?;
769
770        Ok(EnvelopeAccessResult {
771            artifact,
772            artifact_digest,
773            policy_resolution,
774            key_access_plan,
775            plaintext,
776            evidence,
777        })
778    }
779
780    pub fn rewrap_bytes_with_envelope(
781        &self,
782        current_key: &LocalSymmetricKey,
783        new_key: &LocalSymmetricKey,
784        artifact_bytes: &[u8],
785    ) -> Result<EnvelopeRewrapResult, SdkError> {
786        self.rewrap_bytes_with_envelope_using_key_sources(
787            &LocalSymmetricKeySource::from(current_key.clone()),
788            &LocalSymmetricKeySource::from(new_key.clone()),
789            artifact_bytes,
790        )
791    }
792
793    pub fn rewrap_bytes_with_envelope_using_key_sources(
794        &self,
795        current_key_source: &LocalSymmetricKeySource,
796        new_key_source: &LocalSymmetricKeySource,
797        artifact_bytes: &[u8],
798    ) -> Result<EnvelopeRewrapResult, SdkError> {
799        let artifact: LocalEnvelopeArtifact =
800            serde_json::from_slice(artifact_bytes).map_err(|error| {
801                SdkError::Serialization(format!(
802                    "failed to decode local envelope artifact: {error}"
803                ))
804            })?;
805        ensure_local_envelope_artifact_valid(&artifact)?;
806
807        let original_artifact_digest = sha256_prefixed(artifact_bytes);
808        let bootstrap = self.bootstrap()?;
809        ensure_bootstrap_supports_local_rewrap(&bootstrap, ArtifactProfile::Envelope)?;
810
811        let policy_resolution = self.policy_resolve(&SdkPolicyResolveRequest {
812            operation: ProtectionOperation::Rewrap,
813            workload: artifact.workload.clone(),
814            resource: artifact.resource.clone(),
815            content_digest: Some(artifact.content_digest.clone()),
816            content_size_bytes: Some(artifact.content_size_bytes),
817            purpose: artifact.purpose.clone(),
818            labels: artifact.labels.clone(),
819            attributes: artifact.attributes.clone(),
820        })?;
821        ensure_rewrap_policy_resolution_supports_local_only(&policy_resolution)?;
822
823        let protection_plan = self.protection_plan(&SdkProtectionPlanRequest {
824            operation: ProtectionOperation::Rewrap,
825            workload: artifact.workload.clone(),
826            resource: artifact.resource.clone(),
827            preferred_artifact_profile: Some(ArtifactProfile::Envelope),
828            content_digest: Some(artifact.content_digest.clone()),
829            content_size_bytes: Some(artifact.content_size_bytes),
830            purpose: artifact.purpose.clone(),
831            labels: artifact.labels.clone(),
832            attributes: artifact.attributes.clone(),
833        })?;
834        ensure_rewrap_protection_plan_supports_local_only(&protection_plan)?;
835
836        let key_access_plan = self.key_access_plan(&SdkKeyAccessPlanRequest {
837            operation: KeyAccessOperation::Rewrap,
838            workload: artifact.workload.clone(),
839            resource: artifact.resource.clone(),
840            artifact_profile: Some(ArtifactProfile::Envelope),
841            key_reference: Some(
842                current_key_source
843                    .key_reference(LOCAL_ENVELOPE_KEY_REFERENCE)
844                    .to_string(),
845            ),
846            content_digest: Some(artifact.content_digest.clone()),
847            purpose: artifact.purpose.clone(),
848            labels: artifact.labels.clone(),
849            attributes: artifact.attributes.clone(),
850        })?;
851        ensure_key_access_plan_supports_local_crypto(
852            &key_access_plan,
853            KeyAccessOperation::Rewrap,
854            ArtifactProfile::Envelope,
855        )?;
856
857        let current_key = resolve_symmetric_key_for_runtime(
858            self,
859            current_key_source,
860            key_access_plan.execution.key_transport.as_ref(),
861            LOCAL_ENVELOPE_KEY_REFERENCE,
862            "local envelope rewrap",
863        )?;
864        let new_key = resolve_symmetric_key_for_runtime(
865            self,
866            new_key_source,
867            key_access_plan.execution.key_transport.as_ref(),
868            LOCAL_ENVELOPE_KEY_REFERENCE,
869            "local envelope rewrap",
870        )?;
871
872        let plaintext = decrypt_envelope_artifact(&current_key.key, &artifact)?;
873        let decrypted_digest = sha256_prefixed(&plaintext);
874        if decrypted_digest != artifact.content_digest {
875            return Err(SdkError::InvalidInput(
876                "local envelope decrypted bytes do not match the embedded content digest"
877                    .to_string(),
878            ));
879        }
880
881        let decrypted_size = u64::try_from(plaintext.len()).map_err(|_| {
882            SdkError::InvalidInput(
883                "decrypted content length exceeds supported u64 range".to_string(),
884            )
885        })?;
886        if decrypted_size != artifact.content_size_bytes {
887            return Err(SdkError::InvalidInput(
888                "local envelope decrypted bytes do not match the embedded content size".to_string(),
889            ));
890        }
891
892        let content_binding = LocalContentBinding {
893            tenant_id: artifact.tenant_id.clone(),
894            content_digest: artifact.content_digest.clone(),
895            content_size_bytes: artifact.content_size_bytes,
896            raw_cid: artifact.raw_cid.clone(),
897        };
898        let request = LocalProtectionRequest {
899            workload: artifact.workload.clone(),
900            resource: artifact.resource.clone(),
901            preferred_artifact_profile: Some(ArtifactProfile::Envelope),
902            purpose: artifact.purpose.clone(),
903            labels: artifact.labels.clone(),
904            attributes: artifact.attributes.clone(),
905        };
906        let prepared = PreparedLocalProtection {
907            caller: protection_plan.caller.clone(),
908            content_binding: content_binding.clone(),
909            artifact_binding: build_local_artifact_binding(
910                &content_binding,
911                &request,
912                &policy_resolution.handling.bind_policy_to,
913            )?,
914            bootstrap,
915            policy_resolution: policy_resolution.clone(),
916            protection_plan: protection_plan.clone(),
917        };
918
919        let envelope = encrypt_envelope_artifact(&new_key.key, &plaintext, &prepared, &request)?;
920        let new_artifact_bytes = serde_json::to_vec(&envelope).map_err(|error| {
921            SdkError::Serialization(format!(
922                "failed to serialize local envelope artifact: {error}"
923            ))
924        })?;
925        let artifact_digest = sha256_prefixed(&new_artifact_bytes);
926
927        let artifact_registration = self.artifact_register(&SdkArtifactRegisterRequest {
928            operation: ProtectionOperation::Rewrap,
929            workload: artifact.workload.clone(),
930            resource: artifact.resource.clone(),
931            artifact_profile: ArtifactProfile::Envelope,
932            artifact_digest: artifact_digest.clone(),
933            artifact_locator: None,
934            decision_id: None,
935            key_reference: Some(new_key.key_reference),
936            purpose: artifact.purpose.clone(),
937            labels: artifact.labels.clone(),
938            attributes: artifact.attributes.clone(),
939        })?;
940        ensure_artifact_registration_supports_local_only(&artifact_registration)?;
941
942        let evidence = self.evidence(&SdkEvidenceIngestRequest {
943            event_type: EvidenceEventType::Rewrap,
944            workload: artifact.workload.clone(),
945            resource: artifact.resource.clone(),
946            artifact_profile: Some(ArtifactProfile::Envelope),
947            artifact_digest: Some(artifact_digest.clone()),
948            decision_id: None,
949            outcome: Some("success".to_string()),
950            occurred_at: None,
951            purpose: artifact.purpose.clone(),
952            labels: artifact.labels.clone(),
953            attributes: artifact.attributes.clone(),
954        })?;
955        ensure_evidence_ingestion_supports_local_only(&evidence)?;
956
957        Ok(EnvelopeRewrapResult {
958            content_binding,
959            policy_resolution,
960            protection_plan,
961            key_access_plan,
962            original_artifact_digest,
963            artifact: ProtectedEnvelopeArtifact {
964                envelope,
965                artifact_bytes: new_artifact_bytes,
966                artifact_digest,
967            },
968            artifact_registration,
969            evidence,
970        })
971    }
972
973    pub fn protect_bytes_with_tdf(
974        &self,
975        key: &LocalSymmetricKey,
976        plaintext: &[u8],
977        request: LocalProtectionRequest,
978    ) -> Result<TdfProtectionResult, SdkError> {
979        self.protect_bytes_with_tdf_using_key_source(
980            &LocalSymmetricKeySource::from(key.clone()),
981            plaintext,
982            request,
983        )
984    }
985
986    pub fn protect_bytes_with_tdf_using_key_source(
987        &self,
988        key_source: &LocalSymmetricKeySource,
989        plaintext: &[u8],
990        mut request: LocalProtectionRequest,
991    ) -> Result<TdfProtectionResult, SdkError> {
992        request.preferred_artifact_profile = Some(ArtifactProfile::Tdf);
993        let prepared = self.prepare_local_protection(plaintext, request.clone())?;
994
995        let key_access_plan = self.key_access_plan(&SdkKeyAccessPlanRequest {
996            operation: KeyAccessOperation::Wrap,
997            workload: request.workload.clone(),
998            resource: request.resource.clone(),
999            artifact_profile: Some(ArtifactProfile::Tdf),
1000            key_reference: Some(
1001                key_source
1002                    .key_reference(LOCAL_TDF_KEY_REFERENCE)
1003                    .to_string(),
1004            ),
1005            content_digest: Some(prepared.content_binding.content_digest.clone()),
1006            purpose: request.purpose.clone(),
1007            labels: request.labels.clone(),
1008            attributes: request.attributes.clone(),
1009        })?;
1010        ensure_key_access_plan_supports_local_crypto(
1011            &key_access_plan,
1012            KeyAccessOperation::Wrap,
1013            ArtifactProfile::Tdf,
1014        )?;
1015
1016        let resolved_key = resolve_symmetric_key_for_runtime(
1017            self,
1018            key_source,
1019            key_access_plan.execution.key_transport.as_ref(),
1020            LOCAL_TDF_KEY_REFERENCE,
1021            "local TDF protection",
1022        )?;
1023
1024        let tdf = encrypt_tdf_artifact(&resolved_key.key, plaintext, &prepared, &request, 1)?;
1025        let artifact_bytes = serde_json::to_vec(&tdf).map_err(|error| {
1026            SdkError::Serialization(format!("failed to serialize local TDF artifact: {error}"))
1027        })?;
1028        let artifact_digest = sha256_prefixed(&artifact_bytes);
1029
1030        let artifact_registration = self.artifact_register(&SdkArtifactRegisterRequest {
1031            operation: ProtectionOperation::Protect,
1032            workload: request.workload.clone(),
1033            resource: request.resource.clone(),
1034            artifact_profile: ArtifactProfile::Tdf,
1035            artifact_digest: artifact_digest.clone(),
1036            artifact_locator: None,
1037            decision_id: None,
1038            key_reference: Some(resolved_key.key_reference),
1039            purpose: request.purpose.clone(),
1040            labels: request.labels.clone(),
1041            attributes: request.attributes.clone(),
1042        })?;
1043        ensure_artifact_registration_supports_local_only(&artifact_registration)?;
1044
1045        let evidence = self.evidence(&SdkEvidenceIngestRequest {
1046            event_type: EvidenceEventType::Protect,
1047            workload: request.workload,
1048            resource: request.resource,
1049            artifact_profile: Some(ArtifactProfile::Tdf),
1050            artifact_digest: Some(artifact_digest.clone()),
1051            decision_id: None,
1052            outcome: Some("success".to_string()),
1053            occurred_at: None,
1054            purpose: request.purpose,
1055            labels: request.labels,
1056            attributes: request.attributes,
1057        })?;
1058        ensure_evidence_ingestion_supports_local_only(&evidence)?;
1059
1060        Ok(TdfProtectionResult {
1061            prepared,
1062            key_access_plan,
1063            artifact: ProtectedTdfArtifact {
1064                tdf,
1065                artifact_bytes,
1066                artifact_digest,
1067            },
1068            artifact_registration,
1069            evidence,
1070        })
1071    }
1072
1073    pub fn access_bytes_with_tdf(
1074        &self,
1075        key: &LocalSymmetricKey,
1076        artifact_bytes: &[u8],
1077    ) -> Result<TdfAccessResult, SdkError> {
1078        self.access_bytes_with_tdf_using_key_source(
1079            &LocalSymmetricKeySource::from(key.clone()),
1080            artifact_bytes,
1081        )
1082    }
1083
1084    pub fn access_bytes_with_tdf_using_key_source(
1085        &self,
1086        key_source: &LocalSymmetricKeySource,
1087        artifact_bytes: &[u8],
1088    ) -> Result<TdfAccessResult, SdkError> {
1089        let artifact: LocalTdfArtifact =
1090            serde_json::from_slice(artifact_bytes).map_err(|error| {
1091                SdkError::Serialization(format!("failed to decode local TDF artifact: {error}"))
1092            })?;
1093        ensure_local_tdf_artifact_valid(&artifact)?;
1094
1095        let artifact_digest = sha256_prefixed(artifact_bytes);
1096        let (manifest, resolved_key) = match artifact.policy_context.as_ref() {
1097            Some(policy_context) => {
1098                ensure_local_tdf_binding_matches_manifest(&artifact, policy_context)?;
1099                let resolved = resolve_tdf_artifact_key_for_operation(
1100                    self,
1101                    key_source,
1102                    &artifact,
1103                    policy_context,
1104                    ProtectionOperation::Access,
1105                    KeyAccessOperation::Unwrap,
1106                    "local TDF access",
1107                )?;
1108                let manifest = decrypt_tdf_manifest(&resolved.key_material.key, &artifact)?;
1109                ensure_local_tdf_binding_matches_manifest(&artifact, &manifest)?;
1110                ensure_local_tdf_policy_context_matches_manifest(policy_context, &manifest)?;
1111                (manifest, resolved)
1112            }
1113            None => {
1114                let key = require_inline_symmetric_key_without_transport_guidance(
1115                    key_source,
1116                    "local TDF access",
1117                )?;
1118                let manifest = decrypt_tdf_manifest(key, &artifact)?;
1119                ensure_local_tdf_binding_matches_manifest(&artifact, &manifest)?;
1120                let resolved = resolve_tdf_artifact_key_for_operation(
1121                    self,
1122                    key_source,
1123                    &artifact,
1124                    &manifest,
1125                    ProtectionOperation::Access,
1126                    KeyAccessOperation::Unwrap,
1127                    "local TDF access",
1128                )?;
1129                (manifest, resolved)
1130            }
1131        };
1132
1133        let plaintext = decrypt_tdf_payload(&resolved_key.key_material.key, &artifact)?;
1134        let decrypted_digest = sha256_prefixed(&plaintext);
1135        if decrypted_digest != artifact.content_digest {
1136            return Err(SdkError::InvalidInput(
1137                "local TDF decrypted bytes do not match the embedded content digest".to_string(),
1138            ));
1139        }
1140
1141        let decrypted_size = u64::try_from(plaintext.len()).map_err(|_| {
1142            SdkError::InvalidInput(
1143                "decrypted content length exceeds supported u64 range".to_string(),
1144            )
1145        })?;
1146        if decrypted_size != artifact.content_size_bytes {
1147            return Err(SdkError::InvalidInput(
1148                "local TDF decrypted bytes do not match the embedded content size".to_string(),
1149            ));
1150        }
1151
1152        let evidence = self.evidence(&SdkEvidenceIngestRequest {
1153            event_type: EvidenceEventType::Access,
1154            workload: manifest.workload.clone(),
1155            resource: manifest.resource.clone(),
1156            artifact_profile: Some(ArtifactProfile::Tdf),
1157            artifact_digest: Some(artifact_digest.clone()),
1158            decision_id: None,
1159            outcome: Some("success".to_string()),
1160            occurred_at: None,
1161            purpose: manifest.purpose.clone(),
1162            labels: manifest.labels.clone(),
1163            attributes: manifest.attributes.clone(),
1164        })?;
1165        ensure_evidence_ingestion_supports_local_only(&evidence)?;
1166
1167        Ok(TdfAccessResult {
1168            artifact,
1169            manifest,
1170            artifact_digest,
1171            policy_resolution: resolved_key.policy_resolution,
1172            key_access_plan: resolved_key.key_access_plan,
1173            plaintext,
1174            evidence,
1175        })
1176    }
1177
1178    pub fn rewrap_bytes_with_tdf(
1179        &self,
1180        current_key: &LocalSymmetricKey,
1181        new_key: &LocalSymmetricKey,
1182        artifact_bytes: &[u8],
1183    ) -> Result<TdfRewrapResult, SdkError> {
1184        self.rewrap_bytes_with_tdf_using_key_sources(
1185            &LocalSymmetricKeySource::from(current_key.clone()),
1186            &LocalSymmetricKeySource::from(new_key.clone()),
1187            artifact_bytes,
1188        )
1189    }
1190
1191    pub fn rewrap_bytes_with_tdf_using_key_sources(
1192        &self,
1193        current_key_source: &LocalSymmetricKeySource,
1194        new_key_source: &LocalSymmetricKeySource,
1195        artifact_bytes: &[u8],
1196    ) -> Result<TdfRewrapResult, SdkError> {
1197        let artifact: LocalTdfArtifact =
1198            serde_json::from_slice(artifact_bytes).map_err(|error| {
1199                SdkError::Serialization(format!("failed to decode local TDF artifact: {error}"))
1200            })?;
1201        ensure_local_tdf_artifact_valid(&artifact)?;
1202
1203        let manifest = match artifact.policy_context.as_ref() {
1204            Some(policy_context) => {
1205                ensure_local_tdf_binding_matches_manifest(&artifact, policy_context)?;
1206                let manifest = match current_key_source {
1207                    LocalSymmetricKeySource::Inline(key) => decrypt_tdf_manifest(key, &artifact)?,
1208                    LocalSymmetricKeySource::ManagedReference(_) => {
1209                        let resolved = resolve_tdf_artifact_key_for_operation(
1210                            self,
1211                            current_key_source,
1212                            &artifact,
1213                            policy_context,
1214                            ProtectionOperation::Rewrap,
1215                            KeyAccessOperation::Rewrap,
1216                            "local TDF rewrap",
1217                        )?;
1218                        decrypt_tdf_manifest(&resolved.key_material.key, &artifact)?
1219                    }
1220                };
1221                ensure_local_tdf_binding_matches_manifest(&artifact, &manifest)?;
1222                ensure_local_tdf_policy_context_matches_manifest(policy_context, &manifest)?;
1223                manifest
1224            }
1225            None => {
1226                let current_key = require_inline_symmetric_key_for_local_runtime(
1227                    current_key_source,
1228                    None,
1229                    "local TDF rewrap",
1230                )?;
1231                let manifest = decrypt_tdf_manifest(current_key, &artifact)?;
1232                ensure_local_tdf_binding_matches_manifest(&artifact, &manifest)?;
1233                manifest
1234            }
1235        };
1236        let meta_version = artifact.meta_version;
1237
1238        rewrite_tdf_artifact(
1239            self,
1240            current_key_source,
1241            new_key_source,
1242            artifact_bytes,
1243            artifact,
1244            manifest,
1245            meta_version,
1246        )
1247    }
1248
1249    pub fn set_tdf_attributes(
1250        &self,
1251        key: &LocalSymmetricKey,
1252        artifact_bytes: &[u8],
1253        attributes: BTreeMap<String, String>,
1254    ) -> Result<TdfRewrapResult, SdkError> {
1255        self.set_tdf_attributes_using_key_source(
1256            &LocalSymmetricKeySource::from(key.clone()),
1257            artifact_bytes,
1258            attributes,
1259        )
1260    }
1261
1262    pub fn set_tdf_attributes_using_key_source(
1263        &self,
1264        key_source: &LocalSymmetricKeySource,
1265        artifact_bytes: &[u8],
1266        attributes: BTreeMap<String, String>,
1267    ) -> Result<TdfRewrapResult, SdkError> {
1268        let artifact: LocalTdfArtifact =
1269            serde_json::from_slice(artifact_bytes).map_err(|error| {
1270                SdkError::Serialization(format!("failed to decode local TDF artifact: {error}"))
1271            })?;
1272        ensure_local_tdf_artifact_valid(&artifact)?;
1273
1274        let mut manifest = match artifact.policy_context.as_ref() {
1275            Some(policy_context) => {
1276                ensure_local_tdf_binding_matches_manifest(&artifact, policy_context)?;
1277                let manifest = match key_source {
1278                    LocalSymmetricKeySource::Inline(key) => decrypt_tdf_manifest(key, &artifact)?,
1279                    LocalSymmetricKeySource::ManagedReference(_) => {
1280                        let resolved = resolve_tdf_artifact_key_for_operation(
1281                            self,
1282                            key_source,
1283                            &artifact,
1284                            policy_context,
1285                            ProtectionOperation::Rewrap,
1286                            KeyAccessOperation::Rewrap,
1287                            "local TDF attribute rewrite",
1288                        )?;
1289                        decrypt_tdf_manifest(&resolved.key_material.key, &artifact)?
1290                    }
1291                };
1292                ensure_local_tdf_binding_matches_manifest(&artifact, &manifest)?;
1293                ensure_local_tdf_policy_context_matches_manifest(policy_context, &manifest)?;
1294                manifest
1295            }
1296            None => {
1297                let key = require_inline_symmetric_key_without_transport_guidance(
1298                    key_source,
1299                    "local TDF attribute rewrite",
1300                )?;
1301                let manifest = decrypt_tdf_manifest(key, &artifact)?;
1302                ensure_local_tdf_binding_matches_manifest(&artifact, &manifest)?;
1303                manifest
1304            }
1305        };
1306        manifest.attributes = attributes;
1307        let meta_version = artifact.meta_version.saturating_add(1);
1308
1309        rewrite_tdf_artifact(
1310            self,
1311            key_source,
1312            key_source,
1313            artifact_bytes,
1314            artifact,
1315            manifest,
1316            meta_version,
1317        )
1318    }
1319
1320    pub fn edit_tdf_attributes(
1321        &self,
1322        key: &LocalSymmetricKey,
1323        artifact_bytes: &[u8],
1324        edit: LocalAttributeEdit,
1325    ) -> Result<TdfRewrapResult, SdkError> {
1326        self.edit_tdf_attributes_using_key_source(
1327            &LocalSymmetricKeySource::from(key.clone()),
1328            artifact_bytes,
1329            edit,
1330        )
1331    }
1332
1333    pub fn edit_tdf_attributes_using_key_source(
1334        &self,
1335        key_source: &LocalSymmetricKeySource,
1336        artifact_bytes: &[u8],
1337        edit: LocalAttributeEdit,
1338    ) -> Result<TdfRewrapResult, SdkError> {
1339        let artifact: LocalTdfArtifact =
1340            serde_json::from_slice(artifact_bytes).map_err(|error| {
1341                SdkError::Serialization(format!("failed to decode local TDF artifact: {error}"))
1342            })?;
1343        ensure_local_tdf_artifact_valid(&artifact)?;
1344
1345        let mut manifest = match artifact.policy_context.as_ref() {
1346            Some(policy_context) => {
1347                ensure_local_tdf_binding_matches_manifest(&artifact, policy_context)?;
1348                let manifest = match key_source {
1349                    LocalSymmetricKeySource::Inline(key) => decrypt_tdf_manifest(key, &artifact)?,
1350                    LocalSymmetricKeySource::ManagedReference(_) => {
1351                        let resolved = resolve_tdf_artifact_key_for_operation(
1352                            self,
1353                            key_source,
1354                            &artifact,
1355                            policy_context,
1356                            ProtectionOperation::Rewrap,
1357                            KeyAccessOperation::Rewrap,
1358                            "local TDF attribute rewrite",
1359                        )?;
1360                        decrypt_tdf_manifest(&resolved.key_material.key, &artifact)?
1361                    }
1362                };
1363                ensure_local_tdf_binding_matches_manifest(&artifact, &manifest)?;
1364                ensure_local_tdf_policy_context_matches_manifest(policy_context, &manifest)?;
1365                manifest
1366            }
1367            None => {
1368                let key = require_inline_symmetric_key_without_transport_guidance(
1369                    key_source,
1370                    "local TDF attribute rewrite",
1371                )?;
1372                let manifest = decrypt_tdf_manifest(key, &artifact)?;
1373                ensure_local_tdf_binding_matches_manifest(&artifact, &manifest)?;
1374                manifest
1375            }
1376        };
1377        apply_attribute_edit(&mut manifest.attributes, &edit);
1378        let meta_version = artifact.meta_version.saturating_add(1);
1379
1380        rewrite_tdf_artifact(
1381            self,
1382            key_source,
1383            key_source,
1384            artifact_bytes,
1385            artifact,
1386            manifest,
1387            meta_version,
1388        )
1389    }
1390
1391    pub fn sign_bytes_with_detached_signature(
1392        &self,
1393        signing_key: &LocalSigningKey,
1394        content: &[u8],
1395        mut request: LocalProtectionRequest,
1396    ) -> Result<DetachedSignatureSignResult, SdkError> {
1397        request.preferred_artifact_profile = Some(ArtifactProfile::DetachedSignature);
1398        let prepared = self.prepare_local_protection(content, request.clone())?;
1399
1400        let key_access_plan = self.key_access_plan(&SdkKeyAccessPlanRequest {
1401            operation: KeyAccessOperation::Wrap,
1402            workload: request.workload.clone(),
1403            resource: request.resource.clone(),
1404            artifact_profile: Some(ArtifactProfile::DetachedSignature),
1405            key_reference: Some(LOCAL_DETACHED_SIGNATURE_KEY_REFERENCE.to_string()),
1406            content_digest: Some(prepared.content_binding.content_digest.clone()),
1407            purpose: request.purpose.clone(),
1408            labels: request.labels.clone(),
1409            attributes: request.attributes.clone(),
1410        })?;
1411        ensure_key_access_plan_supports_local_crypto(
1412            &key_access_plan,
1413            KeyAccessOperation::Wrap,
1414            ArtifactProfile::DetachedSignature,
1415        )?;
1416
1417        let detached_signature =
1418            sign_detached_signature_artifact(signing_key, &prepared, &request)?;
1419        let artifact_bytes = serde_json::to_vec(&detached_signature).map_err(|error| {
1420            SdkError::Serialization(format!(
1421                "failed to serialize local detached signature artifact: {error}"
1422            ))
1423        })?;
1424        let artifact_digest = sha256_prefixed(&artifact_bytes);
1425
1426        let artifact_registration = self.artifact_register(&SdkArtifactRegisterRequest {
1427            operation: ProtectionOperation::Protect,
1428            workload: request.workload.clone(),
1429            resource: request.resource.clone(),
1430            artifact_profile: ArtifactProfile::DetachedSignature,
1431            artifact_digest: artifact_digest.clone(),
1432            artifact_locator: None,
1433            decision_id: None,
1434            key_reference: Some(LOCAL_DETACHED_SIGNATURE_KEY_REFERENCE.to_string()),
1435            purpose: request.purpose.clone(),
1436            labels: request.labels.clone(),
1437            attributes: request.attributes.clone(),
1438        })?;
1439        ensure_artifact_registration_supports_local_only(&artifact_registration)?;
1440
1441        let evidence = self.evidence(&SdkEvidenceIngestRequest {
1442            event_type: EvidenceEventType::Protect,
1443            workload: request.workload,
1444            resource: request.resource,
1445            artifact_profile: Some(ArtifactProfile::DetachedSignature),
1446            artifact_digest: Some(artifact_digest.clone()),
1447            decision_id: None,
1448            outcome: Some("success".to_string()),
1449            occurred_at: None,
1450            purpose: request.purpose,
1451            labels: request.labels,
1452            attributes: request.attributes,
1453        })?;
1454        ensure_evidence_ingestion_supports_local_only(&evidence)?;
1455
1456        Ok(DetachedSignatureSignResult {
1457            prepared,
1458            key_access_plan,
1459            artifact: ProtectedDetachedSignatureArtifact {
1460                detached_signature,
1461                artifact_bytes,
1462                artifact_digest,
1463            },
1464            artifact_registration,
1465            evidence,
1466        })
1467    }
1468
1469    pub fn verify_bytes_with_detached_signature(
1470        &self,
1471        verifying_key: &LocalVerifyingKey,
1472        content: &[u8],
1473        artifact_bytes: &[u8],
1474    ) -> Result<DetachedSignatureVerifyResult, SdkError> {
1475        let artifact: LocalDetachedSignatureArtifact = serde_json::from_slice(artifact_bytes)
1476            .map_err(|error| {
1477                SdkError::Serialization(format!(
1478                    "failed to decode local detached signature artifact: {error}"
1479                ))
1480            })?;
1481        ensure_local_detached_signature_artifact_valid(&artifact)?;
1482
1483        let artifact_digest = sha256_prefixed(artifact_bytes);
1484        let content_digest = sha256_prefixed(content);
1485        if content_digest != artifact.content_digest {
1486            return Err(SdkError::InvalidInput(
1487                "detached signature content digest does not match the provided content".to_string(),
1488            ));
1489        }
1490
1491        let content_size_bytes = u64::try_from(content.len()).map_err(|_| {
1492            SdkError::InvalidInput("content length exceeds supported u64 range".to_string())
1493        })?;
1494        if content_size_bytes != artifact.content_size_bytes {
1495            return Err(SdkError::InvalidInput(
1496                "detached signature content size does not match the provided content".to_string(),
1497            ));
1498        }
1499
1500        let content_binding = LocalContentBinding {
1501            tenant_id: artifact.tenant_id.clone(),
1502            content_digest: artifact.content_digest.clone(),
1503            content_size_bytes: artifact.content_size_bytes,
1504            raw_cid: artifact.raw_cid.clone(),
1505        };
1506
1507        let policy_resolution = self.policy_resolve(&SdkPolicyResolveRequest {
1508            operation: ProtectionOperation::Access,
1509            workload: artifact.workload.clone(),
1510            resource: artifact.resource.clone(),
1511            content_digest: Some(artifact.content_digest.clone()),
1512            content_size_bytes: Some(artifact.content_size_bytes),
1513            purpose: artifact.purpose.clone(),
1514            labels: artifact.labels.clone(),
1515            attributes: artifact.attributes.clone(),
1516        })?;
1517        ensure_access_policy_resolution_supports_local_access(&policy_resolution)?;
1518
1519        let key_access_plan = self.key_access_plan(&SdkKeyAccessPlanRequest {
1520            operation: KeyAccessOperation::Unwrap,
1521            workload: artifact.workload.clone(),
1522            resource: artifact.resource.clone(),
1523            artifact_profile: Some(ArtifactProfile::DetachedSignature),
1524            key_reference: Some(LOCAL_DETACHED_SIGNATURE_KEY_REFERENCE.to_string()),
1525            content_digest: Some(artifact.content_digest.clone()),
1526            purpose: artifact.purpose.clone(),
1527            labels: artifact.labels.clone(),
1528            attributes: artifact.attributes.clone(),
1529        })?;
1530        ensure_key_access_plan_supports_local_crypto(
1531            &key_access_plan,
1532            KeyAccessOperation::Unwrap,
1533            ArtifactProfile::DetachedSignature,
1534        )?;
1535
1536        let expected_public_key_b64 = BASE64_STANDARD.encode(verifying_key.as_bytes());
1537        if expected_public_key_b64 != artifact.signer_public_key_b64 {
1538            return Err(SdkError::InvalidInput(
1539                "provided verifying key does not match the detached signature artifact signer"
1540                    .to_string(),
1541            ));
1542        }
1543
1544        let expected_signer_key_id = signer_key_id_from_public_key(verifying_key.as_bytes());
1545        if expected_signer_key_id != artifact.signer_key_id {
1546            return Err(SdkError::InvalidInput(
1547                "provided verifying key id does not match the detached signature artifact signer"
1548                    .to_string(),
1549            ));
1550        }
1551
1552        verify_detached_signature_artifact(verifying_key, &artifact, &content_binding)?;
1553
1554        let evidence = self.evidence(&SdkEvidenceIngestRequest {
1555            event_type: EvidenceEventType::Access,
1556            workload: artifact.workload.clone(),
1557            resource: artifact.resource.clone(),
1558            artifact_profile: Some(ArtifactProfile::DetachedSignature),
1559            artifact_digest: Some(artifact_digest.clone()),
1560            decision_id: None,
1561            outcome: Some("success".to_string()),
1562            occurred_at: None,
1563            purpose: artifact.purpose.clone(),
1564            labels: artifact.labels.clone(),
1565            attributes: artifact.attributes.clone(),
1566        })?;
1567        ensure_evidence_ingestion_supports_local_only(&evidence)?;
1568
1569        Ok(DetachedSignatureVerifyResult {
1570            artifact,
1571            artifact_digest,
1572            policy_resolution,
1573            key_access_plan,
1574            content_binding,
1575            evidence,
1576        })
1577    }
1578}
1579
1580fn sha256_prefixed(content: &[u8]) -> String {
1581    let digest = Sha256::digest(content);
1582    format!("sha256:{}", hex::encode(digest))
1583}
1584
1585fn default_binding_version() -> u8 {
1586    1
1587}
1588
1589fn default_meta_version() -> u64 {
1590    1
1591}
1592
1593fn ensure_bootstrap_supports_local_protection(
1594    bootstrap: &SdkBootstrapResponse,
1595    requested_profile: Option<ArtifactProfile>,
1596) -> Result<(), SdkError> {
1597    if bootstrap.plaintext_to_platform {
1598        return Err(SdkError::InvalidInput(
1599            "bootstrap indicates plaintext may be transported to the platform; refusing local-only workflow"
1600                .to_string(),
1601        ));
1602    }
1603
1604    if bootstrap.enforcement_model != "embedded_local_library" {
1605        return Err(SdkError::InvalidInput(format!(
1606            "bootstrap enforcement model {:?} is incompatible with embedded local protection",
1607            bootstrap.enforcement_model
1608        )));
1609    }
1610
1611    if !bootstrap
1612        .supported_operations
1613        .contains(&ProtectionOperation::Protect)
1614    {
1615        return Err(SdkError::InvalidInput(
1616            "bootstrap does not advertise protect support for local workflows".to_string(),
1617        ));
1618    }
1619
1620    if let Some(profile) = requested_profile
1621        && !bootstrap.supported_artifact_profiles.is_empty()
1622        && !bootstrap.supported_artifact_profiles.contains(&profile)
1623    {
1624        return Err(SdkError::InvalidInput(format!(
1625            "requested artifact profile {:?} is not advertised by bootstrap",
1626            profile
1627        )));
1628    }
1629
1630    Ok(())
1631}
1632
1633fn ensure_policy_resolution_supports_local_protection(
1634    policy_resolution: &SdkPolicyResolveResponse,
1635) -> Result<(), SdkError> {
1636    if !policy_resolution.decision.allow {
1637        return Err(SdkError::InvalidInput(
1638            "policy resolution denied the protect operation".to_string(),
1639        ));
1640    }
1641
1642    if !policy_resolution.handling.protect_locally {
1643        return Err(SdkError::InvalidInput(
1644            "policy resolution did not require local enforcement".to_string(),
1645        ));
1646    }
1647
1648    if policy_resolution.handling.plaintext_transport != "forbidden_by_default" {
1649        return Err(SdkError::InvalidInput(format!(
1650            "policy resolution returned unsupported plaintext transport mode {:?}",
1651            policy_resolution.handling.plaintext_transport
1652        )));
1653    }
1654
1655    Ok(())
1656}
1657
1658fn ensure_access_policy_resolution_supports_local_access(
1659    policy_resolution: &SdkPolicyResolveResponse,
1660) -> Result<(), SdkError> {
1661    if !policy_resolution.decision.allow {
1662        return Err(SdkError::InvalidInput(
1663            "policy resolution denied the access operation".to_string(),
1664        ));
1665    }
1666
1667    if !policy_resolution.handling.protect_locally {
1668        return Err(SdkError::InvalidInput(
1669            "policy resolution did not preserve local enforcement for access".to_string(),
1670        ));
1671    }
1672
1673    if policy_resolution.handling.plaintext_transport != "forbidden_by_default" {
1674        return Err(SdkError::InvalidInput(format!(
1675            "policy resolution returned unsupported plaintext transport mode {:?}",
1676            policy_resolution.handling.plaintext_transport
1677        )));
1678    }
1679
1680    Ok(())
1681}
1682
1683fn ensure_rewrap_policy_resolution_supports_local_only(
1684    policy_resolution: &SdkPolicyResolveResponse,
1685) -> Result<(), SdkError> {
1686    if !policy_resolution.decision.allow {
1687        return Err(SdkError::InvalidInput(
1688            "policy resolution denied the rewrap operation".to_string(),
1689        ));
1690    }
1691
1692    if !policy_resolution.handling.protect_locally {
1693        return Err(SdkError::InvalidInput(
1694            "policy resolution did not preserve local enforcement for rewrap".to_string(),
1695        ));
1696    }
1697
1698    if policy_resolution.handling.plaintext_transport != "forbidden_by_default" {
1699        return Err(SdkError::InvalidInput(format!(
1700            "policy resolution returned unsupported plaintext transport mode {:?}",
1701            policy_resolution.handling.plaintext_transport
1702        )));
1703    }
1704
1705    Ok(())
1706}
1707
1708fn ensure_protection_plan_supports_local_protection(
1709    protection_plan: &SdkProtectionPlanResponse,
1710) -> Result<(), SdkError> {
1711    if !protection_plan.decision.allow {
1712        return Err(SdkError::InvalidInput(
1713            "protection plan denied the protect operation".to_string(),
1714        ));
1715    }
1716
1717    if !protection_plan.execution.protect_locally {
1718        return Err(SdkError::InvalidInput(
1719            "protection plan did not require local enforcement".to_string(),
1720        ));
1721    }
1722
1723    if protection_plan.execution.send_plaintext_to_platform {
1724        return Err(SdkError::InvalidInput(
1725            "protection plan requested plaintext transport to the platform; refusing local-only workflow"
1726                .to_string(),
1727        ));
1728    }
1729
1730    if protection_plan.decision.plaintext_transport != "forbidden_by_default" {
1731        return Err(SdkError::InvalidInput(format!(
1732            "protection plan returned unsupported plaintext transport mode {:?}",
1733            protection_plan.decision.plaintext_transport
1734        )));
1735    }
1736
1737    ensure_key_transport_guidance_is_fail_closed(protection_plan.execution.key_transport.as_ref())?;
1738
1739    Ok(())
1740}
1741
1742fn ensure_rewrap_protection_plan_supports_local_only(
1743    protection_plan: &SdkProtectionPlanResponse,
1744) -> Result<(), SdkError> {
1745    if !protection_plan.decision.allow {
1746        return Err(SdkError::InvalidInput(
1747            "protection plan denied the rewrap operation".to_string(),
1748        ));
1749    }
1750
1751    if !protection_plan.execution.protect_locally {
1752        return Err(SdkError::InvalidInput(
1753            "protection plan did not preserve local enforcement for rewrap".to_string(),
1754        ));
1755    }
1756
1757    if protection_plan.execution.send_plaintext_to_platform {
1758        return Err(SdkError::InvalidInput(
1759            "protection plan requested plaintext transport to the platform; refusing local-only rewrap workflow"
1760                .to_string(),
1761        ));
1762    }
1763
1764    if protection_plan.decision.plaintext_transport != "forbidden_by_default" {
1765        return Err(SdkError::InvalidInput(format!(
1766            "protection plan returned unsupported plaintext transport mode {:?}",
1767            protection_plan.decision.plaintext_transport
1768        )));
1769    }
1770
1771    ensure_key_transport_guidance_is_fail_closed(protection_plan.execution.key_transport.as_ref())?;
1772
1773    Ok(())
1774}
1775
1776fn ensure_bootstrap_supports_local_rewrap(
1777    bootstrap: &SdkBootstrapResponse,
1778    requested_profile: ArtifactProfile,
1779) -> Result<(), SdkError> {
1780    if bootstrap.plaintext_to_platform {
1781        return Err(SdkError::InvalidInput(
1782            "bootstrap indicates plaintext may be transported to the platform; refusing local-only rewrap workflow"
1783                .to_string(),
1784        ));
1785    }
1786
1787    if bootstrap.enforcement_model != "embedded_local_library" {
1788        return Err(SdkError::InvalidInput(format!(
1789            "bootstrap enforcement model {:?} is incompatible with embedded local rewrap",
1790            bootstrap.enforcement_model
1791        )));
1792    }
1793
1794    if !bootstrap
1795        .supported_operations
1796        .contains(&ProtectionOperation::Rewrap)
1797    {
1798        return Err(SdkError::InvalidInput(
1799            "bootstrap does not advertise rewrap support for local workflows".to_string(),
1800        ));
1801    }
1802
1803    if !bootstrap.supported_artifact_profiles.is_empty()
1804        && !bootstrap
1805            .supported_artifact_profiles
1806            .contains(&requested_profile)
1807    {
1808        return Err(SdkError::InvalidInput(format!(
1809            "requested artifact profile {:?} is not advertised by bootstrap",
1810            requested_profile
1811        )));
1812    }
1813
1814    Ok(())
1815}
1816
1817fn ensure_key_access_plan_supports_local_crypto(
1818    key_access_plan: &SdkKeyAccessPlanResponse,
1819    expected_operation: KeyAccessOperation,
1820    expected_profile: ArtifactProfile,
1821) -> Result<(), SdkError> {
1822    if !key_access_plan.decision.allow {
1823        return Err(SdkError::InvalidInput(
1824            "key access plan denied the local cryptographic operation".to_string(),
1825        ));
1826    }
1827
1828    if key_access_plan.decision.operation != expected_operation {
1829        return Err(SdkError::InvalidInput(format!(
1830            "key access plan returned unexpected operation {:?}",
1831            key_access_plan.decision.operation
1832        )));
1833    }
1834
1835    if !key_access_plan.execution.local_cryptographic_operation {
1836        return Err(SdkError::InvalidInput(
1837            "key access plan did not allow local cryptographic execution".to_string(),
1838        ));
1839    }
1840
1841    if key_access_plan.execution.send_plaintext_to_platform {
1842        return Err(SdkError::InvalidInput(
1843            "key access plan requested plaintext transport to the platform".to_string(),
1844        ));
1845    }
1846
1847    if key_access_plan.execution.artifact_profile != expected_profile {
1848        return Err(SdkError::InvalidInput(format!(
1849            "key access plan returned unexpected artifact profile {:?}",
1850            key_access_plan.execution.artifact_profile
1851        )));
1852    }
1853
1854    ensure_key_transport_guidance_is_fail_closed(key_access_plan.execution.key_transport.as_ref())?;
1855
1856    Ok(())
1857}
1858
1859fn ensure_key_transport_guidance_is_fail_closed(
1860    key_transport: Option<&KeyTransportGuidance>,
1861) -> Result<(), SdkError> {
1862    if let Some(key_transport) = key_transport
1863        && !key_transport.raw_key_delivery_forbidden
1864    {
1865        return Err(SdkError::InvalidInput(
1866            "key transport guidance permitted raw key delivery; refusing local-only workflow"
1867                .to_string(),
1868        ));
1869    }
1870
1871    Ok(())
1872}
1873
1874fn ensure_key_transport_guidance_supports_local_only(
1875    key_transport: Option<&KeyTransportGuidance>,
1876) -> Result<(), SdkError> {
1877    ensure_key_transport_guidance_is_fail_closed(key_transport)?;
1878
1879    if let Some(key_transport) = key_transport
1880        && key_transport.mode != KeyTransportMode::LocalProvided
1881    {
1882        return Err(SdkError::InvalidInput(format!(
1883            "key transport mode {} requires provider-backed orchestration; current local runtime only supports local_provided",
1884            key_transport_mode_name(key_transport.mode)
1885        )));
1886    }
1887
1888    Ok(())
1889}
1890
1891struct ResolvedSymmetricKeyMaterial {
1892    key: LocalSymmetricKey,
1893    key_reference: String,
1894}
1895
1896struct ResolvedTdfArtifactKey {
1897    policy_resolution: SdkPolicyResolveResponse,
1898    key_access_plan: SdkKeyAccessPlanResponse,
1899    key_material: ResolvedSymmetricKeyMaterial,
1900}
1901
1902fn resolve_symmetric_key_for_runtime(
1903    client: &Client,
1904    key_source: &LocalSymmetricKeySource,
1905    key_transport: Option<&KeyTransportGuidance>,
1906    default_key_reference: &str,
1907    context: &str,
1908) -> Result<ResolvedSymmetricKeyMaterial, SdkError> {
1909    ensure_key_transport_guidance_is_fail_closed(key_transport)?;
1910
1911    let transport_mode = key_transport
1912        .map(|guidance| guidance.mode)
1913        .unwrap_or(KeyTransportMode::LocalProvided);
1914
1915    match transport_mode {
1916        KeyTransportMode::LocalProvided => match key_source {
1917            LocalSymmetricKeySource::Inline(key) => Ok(ResolvedSymmetricKeyMaterial {
1918                key: key.clone(),
1919                key_reference: default_key_reference.to_string(),
1920            }),
1921            LocalSymmetricKeySource::ManagedReference(key_reference) => {
1922                Err(SdkError::InvalidInput(format!(
1923                    "{context} requires an inline symmetric key when key_transport.mode is local_provided; received managed key reference {:?}",
1924                    key_reference.key_reference()
1925                )))
1926            }
1927        },
1928        KeyTransportMode::WrappedKeyReference
1929        | KeyTransportMode::AuthorizedKeyRelease
1930        | KeyTransportMode::KemEncapsulatedCek => match key_source {
1931            LocalSymmetricKeySource::Inline(_) => Err(SdkError::InvalidInput(format!(
1932                "{context} requires a managed key reference when key_transport.mode is {}",
1933                key_transport_mode_name(transport_mode)
1934            ))),
1935            LocalSymmetricKeySource::ManagedReference(key_reference) => {
1936                let provider = client
1937                    .managed_symmetric_key_provider_registry
1938                    .resolve(key_reference.provider_name())?;
1939
1940                if !provider.capabilities().supports(transport_mode) {
1941                    return Err(SdkError::InvalidInput(format!(
1942                        "managed symmetric key provider {:?} does not support key_transport.mode {}",
1943                        provider.provider_name(),
1944                        key_transport_mode_name(transport_mode)
1945                    )));
1946                }
1947
1948                Ok(ResolvedSymmetricKeyMaterial {
1949                    key: provider.resolve_key(key_reference)?,
1950                    key_reference: key_reference.key_reference().to_string(),
1951                })
1952            }
1953        },
1954    }
1955}
1956
1957fn resolve_tdf_artifact_key_for_operation(
1958    client: &Client,
1959    key_source: &LocalSymmetricKeySource,
1960    artifact: &LocalTdfArtifact,
1961    manifest: &LocalTdfManifest,
1962    policy_operation: ProtectionOperation,
1963    key_operation: KeyAccessOperation,
1964    context: &str,
1965) -> Result<ResolvedTdfArtifactKey, SdkError> {
1966    let policy_resolution = client.policy_resolve(&SdkPolicyResolveRequest {
1967        operation: policy_operation,
1968        workload: manifest.workload.clone(),
1969        resource: manifest.resource.clone(),
1970        content_digest: Some(artifact.content_digest.clone()),
1971        content_size_bytes: Some(artifact.content_size_bytes),
1972        purpose: manifest.purpose.clone(),
1973        labels: manifest.labels.clone(),
1974        attributes: manifest.attributes.clone(),
1975    })?;
1976
1977    match policy_operation {
1978        ProtectionOperation::Access => {
1979            ensure_access_policy_resolution_supports_local_access(&policy_resolution)?;
1980        }
1981        ProtectionOperation::Rewrap => {
1982            ensure_rewrap_policy_resolution_supports_local_only(&policy_resolution)?;
1983        }
1984        ProtectionOperation::Protect => {
1985            ensure_policy_resolution_supports_local_protection(&policy_resolution)?;
1986        }
1987    }
1988
1989    let key_access_plan = client.key_access_plan(&SdkKeyAccessPlanRequest {
1990        operation: key_operation,
1991        workload: manifest.workload.clone(),
1992        resource: manifest.resource.clone(),
1993        artifact_profile: Some(ArtifactProfile::Tdf),
1994        key_reference: Some(
1995            key_source
1996                .key_reference(LOCAL_TDF_KEY_REFERENCE)
1997                .to_string(),
1998        ),
1999        content_digest: Some(artifact.content_digest.clone()),
2000        purpose: manifest.purpose.clone(),
2001        labels: manifest.labels.clone(),
2002        attributes: manifest.attributes.clone(),
2003    })?;
2004    ensure_key_access_plan_supports_local_crypto(
2005        &key_access_plan,
2006        key_operation,
2007        ArtifactProfile::Tdf,
2008    )?;
2009
2010    let key_material = resolve_symmetric_key_for_runtime(
2011        client,
2012        key_source,
2013        key_access_plan.execution.key_transport.as_ref(),
2014        LOCAL_TDF_KEY_REFERENCE,
2015        context,
2016    )?;
2017
2018    Ok(ResolvedTdfArtifactKey {
2019        policy_resolution,
2020        key_access_plan,
2021        key_material,
2022    })
2023}
2024
2025fn require_inline_symmetric_key_for_local_runtime<'a>(
2026    key_source: &'a LocalSymmetricKeySource,
2027    key_transport: Option<&KeyTransportGuidance>,
2028    context: &str,
2029) -> Result<&'a LocalSymmetricKey, SdkError> {
2030    ensure_key_transport_guidance_supports_local_only(key_transport)?;
2031
2032    match key_source {
2033        LocalSymmetricKeySource::Inline(key) => Ok(key),
2034        LocalSymmetricKeySource::ManagedReference(key_reference) => {
2035            Err(SdkError::InvalidInput(format!(
2036                "{context} requested managed key reference {key_reference:?}, but provider-backed symmetric key execution is not implemented yet"
2037            )))
2038        }
2039    }
2040}
2041
2042fn require_inline_symmetric_key_without_transport_guidance<'a>(
2043    key_source: &'a LocalSymmetricKeySource,
2044    context: &str,
2045) -> Result<&'a LocalSymmetricKey, SdkError> {
2046    match key_source {
2047        LocalSymmetricKeySource::Inline(key) => Ok(key),
2048        LocalSymmetricKeySource::ManagedReference(key_reference) => {
2049            Err(SdkError::InvalidInput(format!(
2050                "{context} requested managed key reference {:?}, but this workflow still requires inline symmetric key material before runtime key_transport guidance is available",
2051                key_reference.key_reference()
2052            )))
2053        }
2054    }
2055}
2056
2057fn key_transport_mode_name(mode: KeyTransportMode) -> &'static str {
2058    match mode {
2059        KeyTransportMode::LocalProvided => "local_provided",
2060        KeyTransportMode::WrappedKeyReference => "wrapped_key_reference",
2061        KeyTransportMode::AuthorizedKeyRelease => "authorized_key_release",
2062        KeyTransportMode::KemEncapsulatedCek => "kem_encapsulated_cek",
2063    }
2064}
2065
2066fn ensure_artifact_registration_supports_local_only(
2067    artifact_registration: &SdkArtifactRegisterResponse,
2068) -> Result<(), SdkError> {
2069    if !artifact_registration.registration.accepted {
2070        return Err(SdkError::InvalidInput(
2071            "artifact registration was not accepted".to_string(),
2072        ));
2073    }
2074
2075    if artifact_registration
2076        .registration
2077        .send_plaintext_to_platform
2078    {
2079        return Err(SdkError::InvalidInput(
2080            "artifact registration requested plaintext transport to the platform".to_string(),
2081        ));
2082    }
2083
2084    Ok(())
2085}
2086
2087fn ensure_evidence_ingestion_supports_local_only(
2088    evidence: &SdkEvidenceIngestResponse,
2089) -> Result<(), SdkError> {
2090    if !evidence.ingestion.accepted {
2091        return Err(SdkError::InvalidInput(
2092            "evidence ingestion was not accepted".to_string(),
2093        ));
2094    }
2095
2096    if evidence.ingestion.plaintext_transport != "forbidden_by_default" {
2097        return Err(SdkError::InvalidInput(format!(
2098            "evidence ingestion returned unsupported plaintext transport mode {:?}",
2099            evidence.ingestion.plaintext_transport
2100        )));
2101    }
2102
2103    Ok(())
2104}
2105
2106fn ensure_local_envelope_artifact_valid(artifact: &LocalEnvelopeArtifact) -> Result<(), SdkError> {
2107    if artifact.version != 1 {
2108        return Err(SdkError::InvalidInput(format!(
2109            "unsupported local envelope artifact version {}",
2110            artifact.version
2111        )));
2112    }
2113
2114    if artifact.artifact_profile != ArtifactProfile::Envelope {
2115        return Err(SdkError::InvalidInput(format!(
2116            "unsupported artifact profile {:?} for local envelope access",
2117            artifact.artifact_profile
2118        )));
2119    }
2120
2121    Ok(())
2122}
2123
2124fn ensure_local_envelope_binding_matches_artifact(
2125    artifact: &LocalEnvelopeArtifact,
2126) -> Result<(), SdkError> {
2127    let binding = local_artifact_binding_from_envelope_artifact(artifact)?;
2128    if binding.binding_hash != artifact.binding_hash {
2129        return Err(SdkError::InvalidInput(
2130            "local envelope binding hash does not match the embedded metadata binding".to_string(),
2131        ));
2132    }
2133
2134    Ok(())
2135}
2136
2137fn ensure_local_tdf_artifact_valid(artifact: &LocalTdfArtifact) -> Result<(), SdkError> {
2138    if artifact.version != 1 {
2139        return Err(SdkError::InvalidInput(format!(
2140            "unsupported local TDF artifact version {}",
2141            artifact.version
2142        )));
2143    }
2144
2145    if artifact.artifact_profile != ArtifactProfile::Tdf {
2146        return Err(SdkError::InvalidInput(format!(
2147            "unsupported artifact profile {:?} for local TDF access",
2148            artifact.artifact_profile
2149        )));
2150    }
2151
2152    Ok(())
2153}
2154
2155fn ensure_local_tdf_binding_matches_manifest(
2156    artifact: &LocalTdfArtifact,
2157    manifest: &LocalTdfManifest,
2158) -> Result<(), SdkError> {
2159    let binding = local_artifact_binding_from_tdf_artifact(artifact, manifest)?;
2160    if binding.binding_hash != artifact.binding_hash {
2161        return Err(SdkError::InvalidInput(
2162            "local TDF binding hash does not match the embedded metadata binding".to_string(),
2163        ));
2164    }
2165
2166    Ok(())
2167}
2168
2169fn ensure_local_tdf_policy_context_matches_manifest(
2170    policy_context: &LocalTdfManifest,
2171    manifest: &LocalTdfManifest,
2172) -> Result<(), SdkError> {
2173    if policy_context != manifest {
2174        return Err(SdkError::InvalidInput(
2175            "local TDF policy context does not match the decrypted manifest".to_string(),
2176        ));
2177    }
2178
2179    Ok(())
2180}
2181
2182fn ensure_local_detached_signature_artifact_valid(
2183    artifact: &LocalDetachedSignatureArtifact,
2184) -> Result<(), SdkError> {
2185    if artifact.version != 1 {
2186        return Err(SdkError::InvalidInput(format!(
2187            "unsupported local detached signature artifact version {}",
2188            artifact.version
2189        )));
2190    }
2191
2192    if artifact.artifact_profile != ArtifactProfile::DetachedSignature {
2193        return Err(SdkError::InvalidInput(format!(
2194            "unsupported artifact profile {:?} for detached signature verification",
2195            artifact.artifact_profile
2196        )));
2197    }
2198
2199    Ok(())
2200}
2201
2202fn apply_attribute_edit(attributes: &mut BTreeMap<String, String>, edit: &LocalAttributeEdit) {
2203    for key in &edit.remove {
2204        attributes.remove(key);
2205    }
2206
2207    for (key, value) in &edit.set {
2208        attributes.insert(key.clone(), value.clone());
2209    }
2210}
2211
2212fn rewrite_tdf_artifact(
2213    client: &Client,
2214    current_key_source: &LocalSymmetricKeySource,
2215    new_key_source: &LocalSymmetricKeySource,
2216    artifact_bytes: &[u8],
2217    artifact: LocalTdfArtifact,
2218    manifest: LocalTdfManifest,
2219    meta_version: u64,
2220) -> Result<TdfRewrapResult, SdkError> {
2221    let original_artifact_digest = sha256_prefixed(artifact_bytes);
2222
2223    let bootstrap = client.bootstrap()?;
2224    ensure_bootstrap_supports_local_rewrap(&bootstrap, ArtifactProfile::Tdf)?;
2225
2226    let policy_resolution = client.policy_resolve(&SdkPolicyResolveRequest {
2227        operation: ProtectionOperation::Rewrap,
2228        workload: manifest.workload.clone(),
2229        resource: manifest.resource.clone(),
2230        content_digest: Some(artifact.content_digest.clone()),
2231        content_size_bytes: Some(artifact.content_size_bytes),
2232        purpose: manifest.purpose.clone(),
2233        labels: manifest.labels.clone(),
2234        attributes: manifest.attributes.clone(),
2235    })?;
2236    ensure_rewrap_policy_resolution_supports_local_only(&policy_resolution)?;
2237
2238    let protection_plan = client.protection_plan(&SdkProtectionPlanRequest {
2239        operation: ProtectionOperation::Rewrap,
2240        workload: manifest.workload.clone(),
2241        resource: manifest.resource.clone(),
2242        preferred_artifact_profile: Some(ArtifactProfile::Tdf),
2243        content_digest: Some(artifact.content_digest.clone()),
2244        content_size_bytes: Some(artifact.content_size_bytes),
2245        purpose: manifest.purpose.clone(),
2246        labels: manifest.labels.clone(),
2247        attributes: manifest.attributes.clone(),
2248    })?;
2249    ensure_rewrap_protection_plan_supports_local_only(&protection_plan)?;
2250
2251    let key_access_plan = client.key_access_plan(&SdkKeyAccessPlanRequest {
2252        operation: KeyAccessOperation::Rewrap,
2253        workload: manifest.workload.clone(),
2254        resource: manifest.resource.clone(),
2255        artifact_profile: Some(ArtifactProfile::Tdf),
2256        key_reference: Some(
2257            current_key_source
2258                .key_reference(LOCAL_TDF_KEY_REFERENCE)
2259                .to_string(),
2260        ),
2261        content_digest: Some(artifact.content_digest.clone()),
2262        purpose: manifest.purpose.clone(),
2263        labels: manifest.labels.clone(),
2264        attributes: manifest.attributes.clone(),
2265    })?;
2266    ensure_key_access_plan_supports_local_crypto(
2267        &key_access_plan,
2268        KeyAccessOperation::Rewrap,
2269        ArtifactProfile::Tdf,
2270    )?;
2271
2272    let current_key = resolve_symmetric_key_for_runtime(
2273        client,
2274        current_key_source,
2275        key_access_plan.execution.key_transport.as_ref(),
2276        LOCAL_TDF_KEY_REFERENCE,
2277        "local TDF rewrap",
2278    )?;
2279    let new_key = resolve_symmetric_key_for_runtime(
2280        client,
2281        new_key_source,
2282        key_access_plan.execution.key_transport.as_ref(),
2283        LOCAL_TDF_KEY_REFERENCE,
2284        "local TDF rewrap",
2285    )?;
2286
2287    let plaintext = decrypt_tdf_payload(&current_key.key, &artifact)?;
2288    let decrypted_digest = sha256_prefixed(&plaintext);
2289    if decrypted_digest != artifact.content_digest {
2290        return Err(SdkError::InvalidInput(
2291            "local TDF decrypted bytes do not match the embedded content digest".to_string(),
2292        ));
2293    }
2294
2295    let decrypted_size = u64::try_from(plaintext.len()).map_err(|_| {
2296        SdkError::InvalidInput("decrypted content length exceeds supported u64 range".to_string())
2297    })?;
2298    if decrypted_size != artifact.content_size_bytes {
2299        return Err(SdkError::InvalidInput(
2300            "local TDF decrypted bytes do not match the embedded content size".to_string(),
2301        ));
2302    }
2303
2304    let content_binding = LocalContentBinding {
2305        tenant_id: artifact.tenant_id.clone(),
2306        content_digest: artifact.content_digest.clone(),
2307        content_size_bytes: artifact.content_size_bytes,
2308        raw_cid: artifact.raw_cid.clone(),
2309    };
2310    let request = LocalProtectionRequest {
2311        workload: manifest.workload.clone(),
2312        resource: manifest.resource.clone(),
2313        preferred_artifact_profile: Some(ArtifactProfile::Tdf),
2314        purpose: manifest.purpose.clone(),
2315        labels: manifest.labels.clone(),
2316        attributes: manifest.attributes.clone(),
2317    };
2318    let prepared = PreparedLocalProtection {
2319        caller: protection_plan.caller.clone(),
2320        content_binding: content_binding.clone(),
2321        artifact_binding: build_local_artifact_binding(
2322            &content_binding,
2323            &request,
2324            &policy_resolution.handling.bind_policy_to,
2325        )?,
2326        bootstrap,
2327        policy_resolution: policy_resolution.clone(),
2328        protection_plan: protection_plan.clone(),
2329    };
2330
2331    let tdf = encrypt_tdf_artifact(&new_key.key, &plaintext, &prepared, &request, meta_version)?;
2332    let new_artifact_bytes = serde_json::to_vec(&tdf).map_err(|error| {
2333        SdkError::Serialization(format!("failed to serialize local TDF artifact: {error}"))
2334    })?;
2335    let artifact_digest = sha256_prefixed(&new_artifact_bytes);
2336
2337    let artifact_registration = client.artifact_register(&SdkArtifactRegisterRequest {
2338        operation: ProtectionOperation::Rewrap,
2339        workload: manifest.workload.clone(),
2340        resource: manifest.resource.clone(),
2341        artifact_profile: ArtifactProfile::Tdf,
2342        artifact_digest: artifact_digest.clone(),
2343        artifact_locator: None,
2344        decision_id: None,
2345        key_reference: Some(new_key.key_reference),
2346        purpose: manifest.purpose.clone(),
2347        labels: manifest.labels.clone(),
2348        attributes: manifest.attributes.clone(),
2349    })?;
2350    ensure_artifact_registration_supports_local_only(&artifact_registration)?;
2351
2352    let evidence = client.evidence(&SdkEvidenceIngestRequest {
2353        event_type: EvidenceEventType::Rewrap,
2354        workload: manifest.workload.clone(),
2355        resource: manifest.resource.clone(),
2356        artifact_profile: Some(ArtifactProfile::Tdf),
2357        artifact_digest: Some(artifact_digest.clone()),
2358        decision_id: None,
2359        outcome: Some("success".to_string()),
2360        occurred_at: None,
2361        purpose: manifest.purpose.clone(),
2362        labels: manifest.labels.clone(),
2363        attributes: manifest.attributes.clone(),
2364    })?;
2365    ensure_evidence_ingestion_supports_local_only(&evidence)?;
2366
2367    Ok(TdfRewrapResult {
2368        content_binding,
2369        manifest,
2370        policy_resolution,
2371        protection_plan,
2372        key_access_plan,
2373        original_artifact_digest,
2374        artifact: ProtectedTdfArtifact {
2375            tdf,
2376            artifact_bytes: new_artifact_bytes,
2377            artifact_digest,
2378        },
2379        artifact_registration,
2380        evidence,
2381    })
2382}
2383
2384fn encrypt_envelope_artifact(
2385    key: &LocalSymmetricKey,
2386    plaintext: &[u8],
2387    prepared: &PreparedLocalProtection,
2388    request: &LocalProtectionRequest,
2389) -> Result<LocalEnvelopeArtifact, SdkError> {
2390    let binding_hash = prepared.artifact_binding.binding_hash.clone();
2391    let aad = envelope_aad_bytes(&EnvelopeAadBinding {
2392        tenant_id: &prepared.content_binding.tenant_id,
2393        raw_cid: &prepared.content_binding.raw_cid,
2394        content_digest: &prepared.content_binding.content_digest,
2395        artifact_profile: ArtifactProfile::Envelope,
2396        workload: &request.workload,
2397        resource: &request.resource,
2398        purpose: &request.purpose,
2399        labels: &request.labels,
2400        attributes: &request.attributes,
2401        binding_targets: &prepared.artifact_binding.binding_targets,
2402        binding_hash: &binding_hash,
2403    })?;
2404    let aad_hash = sha256_prefixed(&aad);
2405
2406    let mut nonce_bytes = [0u8; 12];
2407    rand::rngs::OsRng.fill_bytes(&mut nonce_bytes);
2408    let nonce = Nonce::from_slice(&nonce_bytes);
2409
2410    let cipher = Aes256Gcm::new_from_slice(key.as_bytes()).map_err(|error| {
2411        SdkError::InvalidInput(format!("failed to initialize envelope cipher: {error}"))
2412    })?;
2413    let ciphertext = cipher
2414        .encrypt(
2415            nonce,
2416            aes_gcm::aead::Payload {
2417                msg: plaintext,
2418                aad: &aad,
2419            },
2420        )
2421        .map_err(|error| SdkError::Server(format!("local envelope encryption failed: {error}")))?;
2422
2423    Ok(LocalEnvelopeArtifact {
2424        version: 1,
2425        artifact_profile: ArtifactProfile::Envelope,
2426        algorithm: LocalEnvelopeAlgorithm::Aes256Gcm,
2427        tenant_id: prepared.content_binding.tenant_id.clone(),
2428        raw_cid: prepared.content_binding.raw_cid.clone(),
2429        content_digest: prepared.content_binding.content_digest.clone(),
2430        content_size_bytes: prepared.content_binding.content_size_bytes,
2431        workload: request.workload.clone(),
2432        resource: request.resource.clone(),
2433        purpose: request.purpose.clone(),
2434        labels: request.labels.clone(),
2435        attributes: request.attributes.clone(),
2436        binding_targets: prepared.artifact_binding.binding_targets.clone(),
2437        binding_hash,
2438        nonce_b64: BASE64_STANDARD.encode(nonce_bytes),
2439        aad_hash,
2440        ciphertext_b64: BASE64_STANDARD.encode(ciphertext),
2441    })
2442}
2443
2444fn decrypt_envelope_artifact(
2445    key: &LocalSymmetricKey,
2446    artifact: &LocalEnvelopeArtifact,
2447) -> Result<Vec<u8>, SdkError> {
2448    let aad = envelope_aad_bytes(&EnvelopeAadBinding {
2449        tenant_id: &artifact.tenant_id,
2450        raw_cid: &artifact.raw_cid,
2451        content_digest: &artifact.content_digest,
2452        artifact_profile: ArtifactProfile::Envelope,
2453        workload: &artifact.workload,
2454        resource: &artifact.resource,
2455        purpose: &artifact.purpose,
2456        labels: &artifact.labels,
2457        attributes: &artifact.attributes,
2458        binding_targets: &artifact.binding_targets,
2459        binding_hash: &artifact.binding_hash,
2460    })?;
2461
2462    let expected_aad_hash = sha256_prefixed(&aad);
2463    if expected_aad_hash != artifact.aad_hash {
2464        return Err(SdkError::InvalidInput(
2465            "local envelope artifact AAD hash does not match the embedded metadata binding"
2466                .to_string(),
2467        ));
2468    }
2469
2470    ensure_local_envelope_binding_matches_artifact(artifact)?;
2471
2472    let nonce_bytes = BASE64_STANDARD
2473        .decode(&artifact.nonce_b64)
2474        .map_err(|error| {
2475            SdkError::Serialization(format!("failed to decode local envelope nonce: {error}"))
2476        })?;
2477    let ciphertext = BASE64_STANDARD
2478        .decode(&artifact.ciphertext_b64)
2479        .map_err(|error| {
2480            SdkError::Serialization(format!(
2481                "failed to decode local envelope ciphertext: {error}"
2482            ))
2483        })?;
2484    let nonce_array: [u8; 12] = nonce_bytes.try_into().map_err(|_| {
2485        SdkError::Serialization("local envelope nonce must be 12 bytes".to_string())
2486    })?;
2487
2488    let cipher = Aes256Gcm::new_from_slice(key.as_bytes()).map_err(|error| {
2489        SdkError::InvalidInput(format!("failed to initialize envelope cipher: {error}"))
2490    })?;
2491    cipher
2492        .decrypt(
2493            Nonce::from_slice(&nonce_array),
2494            aes_gcm::aead::Payload {
2495                msg: &ciphertext,
2496                aad: &aad,
2497            },
2498        )
2499        .map_err(|error| SdkError::Server(format!("local envelope decryption failed: {error}")))
2500}
2501
2502fn encrypt_tdf_artifact(
2503    key: &LocalSymmetricKey,
2504    plaintext: &[u8],
2505    prepared: &PreparedLocalProtection,
2506    request: &LocalProtectionRequest,
2507    meta_version: u64,
2508) -> Result<LocalTdfArtifact, SdkError> {
2509    let manifest = LocalTdfManifest {
2510        workload: request.workload.clone(),
2511        resource: request.resource.clone(),
2512        purpose: request.purpose.clone(),
2513        labels: request.labels.clone(),
2514        attributes: request.attributes.clone(),
2515    };
2516    let manifest_bytes = serde_json::to_vec(&manifest).map_err(|error| {
2517        SdkError::Serialization(format!("failed to serialize local TDF manifest: {error}"))
2518    })?;
2519    let manifest_digest = sha256_prefixed(&manifest_bytes);
2520    let binding_hash = prepared.artifact_binding.binding_hash.clone();
2521    let aad = tdf_aad_bytes(
2522        &prepared.content_binding.tenant_id,
2523        &prepared.content_binding.raw_cid,
2524        &prepared.content_binding.content_digest,
2525        prepared.content_binding.content_size_bytes,
2526        &manifest_digest,
2527        &binding_hash,
2528    )?;
2529    let aad_hash = sha256_prefixed(&aad);
2530
2531    let cipher = Aes256Gcm::new_from_slice(key.as_bytes()).map_err(|error| {
2532        SdkError::InvalidInput(format!("failed to initialize TDF cipher: {error}"))
2533    })?;
2534
2535    let mut manifest_nonce_bytes = [0u8; 12];
2536    rand::rngs::OsRng.fill_bytes(&mut manifest_nonce_bytes);
2537    let manifest_nonce = Nonce::from_slice(&manifest_nonce_bytes);
2538    let manifest_ciphertext = cipher
2539        .encrypt(
2540            manifest_nonce,
2541            aes_gcm::aead::Payload {
2542                msg: &manifest_bytes,
2543                aad: &aad,
2544            },
2545        )
2546        .map_err(|error| {
2547            SdkError::Server(format!("local TDF manifest encryption failed: {error}"))
2548        })?;
2549
2550    let mut payload_nonce_bytes = [0u8; 12];
2551    rand::rngs::OsRng.fill_bytes(&mut payload_nonce_bytes);
2552    let payload_nonce = Nonce::from_slice(&payload_nonce_bytes);
2553    let payload_ciphertext = cipher
2554        .encrypt(
2555            payload_nonce,
2556            aes_gcm::aead::Payload {
2557                msg: plaintext,
2558                aad: &aad,
2559            },
2560        )
2561        .map_err(|error| {
2562            SdkError::Server(format!("local TDF payload encryption failed: {error}"))
2563        })?;
2564
2565    Ok(LocalTdfArtifact {
2566        version: 1,
2567        meta_version,
2568        artifact_profile: ArtifactProfile::Tdf,
2569        algorithm: LocalTdfAlgorithm::Aes256Gcm,
2570        tenant_id: prepared.content_binding.tenant_id.clone(),
2571        raw_cid: prepared.content_binding.raw_cid.clone(),
2572        content_digest: prepared.content_binding.content_digest.clone(),
2573        content_size_bytes: prepared.content_binding.content_size_bytes,
2574        manifest_digest,
2575        binding_targets: prepared.artifact_binding.binding_targets.clone(),
2576        binding_hash,
2577        policy_context: Some(manifest),
2578        manifest_nonce_b64: BASE64_STANDARD.encode(manifest_nonce_bytes),
2579        manifest_ciphertext_b64: BASE64_STANDARD.encode(manifest_ciphertext),
2580        payload_nonce_b64: BASE64_STANDARD.encode(payload_nonce_bytes),
2581        payload_ciphertext_b64: BASE64_STANDARD.encode(payload_ciphertext),
2582        aad_hash,
2583    })
2584}
2585
2586fn decrypt_tdf_manifest(
2587    key: &LocalSymmetricKey,
2588    artifact: &LocalTdfArtifact,
2589) -> Result<LocalTdfManifest, SdkError> {
2590    let aad = tdf_aad_bytes(
2591        &artifact.tenant_id,
2592        &artifact.raw_cid,
2593        &artifact.content_digest,
2594        artifact.content_size_bytes,
2595        &artifact.manifest_digest,
2596        &artifact.binding_hash,
2597    )?;
2598    let expected_aad_hash = sha256_prefixed(&aad);
2599    if expected_aad_hash != artifact.aad_hash {
2600        return Err(SdkError::InvalidInput(
2601            "local TDF artifact AAD hash does not match the embedded metadata binding".to_string(),
2602        ));
2603    }
2604
2605    let nonce_bytes = BASE64_STANDARD
2606        .decode(&artifact.manifest_nonce_b64)
2607        .map_err(|error| {
2608            SdkError::Serialization(format!(
2609                "failed to decode local TDF manifest nonce: {error}"
2610            ))
2611        })?;
2612    let ciphertext = BASE64_STANDARD
2613        .decode(&artifact.manifest_ciphertext_b64)
2614        .map_err(|error| {
2615            SdkError::Serialization(format!(
2616                "failed to decode local TDF manifest ciphertext: {error}"
2617            ))
2618        })?;
2619    let nonce_array: [u8; 12] = nonce_bytes.try_into().map_err(|_| {
2620        SdkError::Serialization("local TDF manifest nonce must be 12 bytes".to_string())
2621    })?;
2622
2623    let cipher = Aes256Gcm::new_from_slice(key.as_bytes()).map_err(|error| {
2624        SdkError::InvalidInput(format!("failed to initialize TDF cipher: {error}"))
2625    })?;
2626    let manifest_bytes = cipher
2627        .decrypt(
2628            Nonce::from_slice(&nonce_array),
2629            aes_gcm::aead::Payload {
2630                msg: &ciphertext,
2631                aad: &aad,
2632            },
2633        )
2634        .map_err(|error| {
2635            SdkError::Server(format!("local TDF manifest decryption failed: {error}"))
2636        })?;
2637
2638    let manifest_digest = sha256_prefixed(&manifest_bytes);
2639    if manifest_digest != artifact.manifest_digest {
2640        return Err(SdkError::InvalidInput(
2641            "local TDF manifest digest does not match the embedded metadata binding".to_string(),
2642        ));
2643    }
2644
2645    serde_json::from_slice(&manifest_bytes).map_err(|error| {
2646        SdkError::Serialization(format!("failed to decode local TDF manifest: {error}"))
2647    })
2648}
2649
2650fn decrypt_tdf_payload(
2651    key: &LocalSymmetricKey,
2652    artifact: &LocalTdfArtifact,
2653) -> Result<Vec<u8>, SdkError> {
2654    let aad = tdf_aad_bytes(
2655        &artifact.tenant_id,
2656        &artifact.raw_cid,
2657        &artifact.content_digest,
2658        artifact.content_size_bytes,
2659        &artifact.manifest_digest,
2660        &artifact.binding_hash,
2661    )?;
2662    let expected_aad_hash = sha256_prefixed(&aad);
2663    if expected_aad_hash != artifact.aad_hash {
2664        return Err(SdkError::InvalidInput(
2665            "local TDF artifact AAD hash does not match the embedded metadata binding".to_string(),
2666        ));
2667    }
2668
2669    let nonce_bytes = BASE64_STANDARD
2670        .decode(&artifact.payload_nonce_b64)
2671        .map_err(|error| {
2672            SdkError::Serialization(format!("failed to decode local TDF payload nonce: {error}"))
2673        })?;
2674    let ciphertext = BASE64_STANDARD
2675        .decode(&artifact.payload_ciphertext_b64)
2676        .map_err(|error| {
2677            SdkError::Serialization(format!(
2678                "failed to decode local TDF payload ciphertext: {error}"
2679            ))
2680        })?;
2681    let nonce_array: [u8; 12] = nonce_bytes.try_into().map_err(|_| {
2682        SdkError::Serialization("local TDF payload nonce must be 12 bytes".to_string())
2683    })?;
2684
2685    let cipher = Aes256Gcm::new_from_slice(key.as_bytes()).map_err(|error| {
2686        SdkError::InvalidInput(format!("failed to initialize TDF cipher: {error}"))
2687    })?;
2688    cipher
2689        .decrypt(
2690            Nonce::from_slice(&nonce_array),
2691            aes_gcm::aead::Payload {
2692                msg: &ciphertext,
2693                aad: &aad,
2694            },
2695        )
2696        .map_err(|error| SdkError::Server(format!("local TDF payload decryption failed: {error}")))
2697}
2698
2699fn sign_detached_signature_artifact(
2700    signing_key: &LocalSigningKey,
2701    prepared: &PreparedLocalProtection,
2702    request: &LocalProtectionRequest,
2703) -> Result<LocalDetachedSignatureArtifact, SdkError> {
2704    let binding_bytes = local_artifact_binding_bytes(&prepared.artifact_binding)?;
2705    let binding_hash = prepared.artifact_binding.binding_hash.clone();
2706
2707    let signer = Ed25519SigningKey::from_bytes(signing_key.as_bytes());
2708    let verifying_key = signer.verifying_key();
2709    let signature = signer.sign(&binding_bytes);
2710    let public_key_bytes = verifying_key.to_bytes();
2711
2712    Ok(LocalDetachedSignatureArtifact {
2713        version: 1,
2714        artifact_profile: ArtifactProfile::DetachedSignature,
2715        algorithm: LocalDetachedSignatureAlgorithm::Ed25519,
2716        tenant_id: prepared.content_binding.tenant_id.clone(),
2717        raw_cid: prepared.content_binding.raw_cid.clone(),
2718        content_digest: prepared.content_binding.content_digest.clone(),
2719        content_size_bytes: prepared.content_binding.content_size_bytes,
2720        workload: request.workload.clone(),
2721        resource: request.resource.clone(),
2722        purpose: request.purpose.clone(),
2723        labels: request.labels.clone(),
2724        attributes: request.attributes.clone(),
2725        binding_targets: prepared.artifact_binding.binding_targets.clone(),
2726        signer_key_id: signer_key_id_from_public_key(&public_key_bytes),
2727        signer_public_key_b64: BASE64_STANDARD.encode(public_key_bytes),
2728        binding_hash,
2729        signature_b64: BASE64_STANDARD.encode(signature.to_bytes()),
2730    })
2731}
2732
2733fn verify_detached_signature_artifact(
2734    verifying_key: &LocalVerifyingKey,
2735    artifact: &LocalDetachedSignatureArtifact,
2736    content_binding: &LocalContentBinding,
2737) -> Result<(), SdkError> {
2738    let binding =
2739        local_artifact_binding_from_detached_signature_artifact(artifact, content_binding)?;
2740    let binding_bytes = local_artifact_binding_bytes(&binding)?;
2741    let expected_binding_hash = binding.binding_hash;
2742    if expected_binding_hash != artifact.binding_hash {
2743        return Err(SdkError::InvalidInput(
2744            "detached signature binding hash does not match the embedded metadata binding"
2745                .to_string(),
2746        ));
2747    }
2748
2749    let signature_bytes = BASE64_STANDARD
2750        .decode(&artifact.signature_b64)
2751        .map_err(|error| {
2752            SdkError::Serialization(format!(
2753                "failed to decode detached signature bytes: {error}"
2754            ))
2755        })?;
2756    let signature = Ed25519Signature::from_slice(&signature_bytes).map_err(|error| {
2757        SdkError::Serialization(format!("invalid detached signature bytes: {error}"))
2758    })?;
2759
2760    let verifying_key =
2761        Ed25519VerifyingKey::from_bytes(verifying_key.as_bytes()).map_err(|error| {
2762            SdkError::InvalidInput(format!("invalid Ed25519 verifying key: {error}"))
2763        })?;
2764    verifying_key
2765        .verify(&binding_bytes, &signature)
2766        .map_err(|error| {
2767            SdkError::InvalidInput(format!("detached signature verification failed: {error}"))
2768        })
2769}
2770
2771fn envelope_aad_bytes(binding: &EnvelopeAadBinding<'_>) -> Result<Vec<u8>, SdkError> {
2772    serde_json::to_vec(binding).map_err(|error| {
2773        SdkError::Serialization(format!("failed to serialize envelope AAD binding: {error}"))
2774    })
2775}
2776
2777fn tdf_aad_bytes(
2778    tenant_id: &str,
2779    raw_cid: &str,
2780    content_digest: &str,
2781    content_size_bytes: u64,
2782    manifest_digest: &str,
2783    binding_hash: &str,
2784) -> Result<Vec<u8>, SdkError> {
2785    serde_json::to_vec(&TdfAadBinding {
2786        tenant_id,
2787        raw_cid,
2788        content_digest,
2789        content_size_bytes,
2790        manifest_digest,
2791        artifact_profile: ArtifactProfile::Tdf,
2792        binding_hash,
2793    })
2794    .map_err(|error| {
2795        SdkError::Serialization(format!("failed to serialize TDF AAD binding: {error}"))
2796    })
2797}
2798
2799fn build_local_artifact_binding(
2800    content_binding: &LocalContentBinding,
2801    request: &LocalProtectionRequest,
2802    binding_targets: &[String],
2803) -> Result<LocalArtifactBinding, SdkError> {
2804    let mut artifact_binding = LocalArtifactBinding {
2805        version: default_binding_version(),
2806        tenant_id: content_binding.tenant_id.clone(),
2807        raw_cid: content_binding.raw_cid.clone(),
2808        content_digest: content_binding.content_digest.clone(),
2809        content_size_bytes: content_binding.content_size_bytes,
2810        workload: request.workload.clone(),
2811        resource: request.resource.clone(),
2812        purpose: request.purpose.clone(),
2813        labels: request.labels.clone(),
2814        attributes: request.attributes.clone(),
2815        binding_targets: binding_targets.to_vec(),
2816        binding_hash: String::new(),
2817    };
2818    artifact_binding.binding_hash =
2819        sha256_prefixed(&local_artifact_binding_bytes(&artifact_binding)?);
2820    Ok(artifact_binding)
2821}
2822
2823fn local_artifact_binding_bytes(binding: &LocalArtifactBinding) -> Result<Vec<u8>, SdkError> {
2824    serde_json::to_vec(&LocalArtifactBindingPayload {
2825        version: binding.version,
2826        tenant_id: &binding.tenant_id,
2827        raw_cid: &binding.raw_cid,
2828        content_digest: &binding.content_digest,
2829        content_size_bytes: binding.content_size_bytes,
2830        workload: &binding.workload,
2831        resource: &binding.resource,
2832        purpose: &binding.purpose,
2833        labels: &binding.labels,
2834        attributes: &binding.attributes,
2835        binding_targets: &binding.binding_targets,
2836    })
2837    .map_err(|error| {
2838        SdkError::Serialization(format!(
2839            "failed to serialize local artifact binding: {error}"
2840        ))
2841    })
2842}
2843
2844fn local_artifact_binding_from_envelope_artifact(
2845    artifact: &LocalEnvelopeArtifact,
2846) -> Result<LocalArtifactBinding, SdkError> {
2847    build_local_artifact_binding(
2848        &LocalContentBinding {
2849            tenant_id: artifact.tenant_id.clone(),
2850            content_digest: artifact.content_digest.clone(),
2851            content_size_bytes: artifact.content_size_bytes,
2852            raw_cid: artifact.raw_cid.clone(),
2853        },
2854        &LocalProtectionRequest {
2855            workload: artifact.workload.clone(),
2856            resource: artifact.resource.clone(),
2857            preferred_artifact_profile: Some(ArtifactProfile::Envelope),
2858            purpose: artifact.purpose.clone(),
2859            labels: artifact.labels.clone(),
2860            attributes: artifact.attributes.clone(),
2861        },
2862        &artifact.binding_targets,
2863    )
2864}
2865
2866fn local_artifact_binding_from_tdf_artifact(
2867    artifact: &LocalTdfArtifact,
2868    manifest: &LocalTdfManifest,
2869) -> Result<LocalArtifactBinding, SdkError> {
2870    build_local_artifact_binding(
2871        &LocalContentBinding {
2872            tenant_id: artifact.tenant_id.clone(),
2873            content_digest: artifact.content_digest.clone(),
2874            content_size_bytes: artifact.content_size_bytes,
2875            raw_cid: artifact.raw_cid.clone(),
2876        },
2877        &LocalProtectionRequest {
2878            workload: manifest.workload.clone(),
2879            resource: manifest.resource.clone(),
2880            preferred_artifact_profile: Some(ArtifactProfile::Tdf),
2881            purpose: manifest.purpose.clone(),
2882            labels: manifest.labels.clone(),
2883            attributes: manifest.attributes.clone(),
2884        },
2885        &artifact.binding_targets,
2886    )
2887}
2888
2889fn local_artifact_binding_from_detached_signature_artifact(
2890    artifact: &LocalDetachedSignatureArtifact,
2891    content_binding: &LocalContentBinding,
2892) -> Result<LocalArtifactBinding, SdkError> {
2893    build_local_artifact_binding(
2894        content_binding,
2895        &LocalProtectionRequest {
2896            workload: artifact.workload.clone(),
2897            resource: artifact.resource.clone(),
2898            preferred_artifact_profile: Some(ArtifactProfile::DetachedSignature),
2899            purpose: artifact.purpose.clone(),
2900            labels: artifact.labels.clone(),
2901            attributes: artifact.attributes.clone(),
2902        },
2903        &artifact.binding_targets,
2904    )
2905}
2906
2907fn signer_key_id_from_public_key(public_key_bytes: &[u8]) -> String {
2908    sha256_prefixed(public_key_bytes)
2909}