Skip to main content

sdk_rust/
integration_full_runner.rs

1use std::{collections::BTreeMap, fs, path::Path};
2
3use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
4use serde::{Deserialize, Serialize};
5use serde_json::{Value, json};
6
7use crate::{
8    Client, SdkError,
9    local::{
10        LocalProtectionRequest, LocalSigningKey, LocalSymmetricKey, LocalSymmetricKeySource,
11        LocalVerifyingKey,
12    },
13    models::{
14        ArtifactProfile, EvidenceEventType, KeyAccessOperation, KeyTransportGuidance,
15        KeyTransportMode, ProtectionOperation, ResourceDescriptor, SdkArtifactRegisterRequest,
16        SdkEvidenceIngestRequest, SdkKeyAccessPlanRequest, SdkPolicyResolveRequest,
17        SdkProtectionPlanRequest, WorkloadDescriptor,
18    },
19};
20
21#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
22pub struct IntegrationFullScenarioStep {
23    pub name: String,
24    pub description: String,
25    #[serde(default)]
26    pub sdk_methods: Vec<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
30pub struct IntegrationFullManifest {
31    pub name: String,
32    pub version: u32,
33    pub description: String,
34    pub tenant_id: String,
35    pub principal_id: String,
36    pub subject: String,
37    #[serde(default)]
38    pub default_required_scopes: Vec<String>,
39    pub policy_scope: String,
40    pub workload: WorkloadDescriptor,
41    pub resource: ResourceDescriptor,
42    pub purpose: String,
43    pub plaintext_utf8: String,
44    pub content_digest: String,
45    #[serde(default)]
46    pub labels: Vec<String>,
47    #[serde(default)]
48    pub attributes: BTreeMap<String, String>,
49    pub inline_key_b64: String,
50    pub rewrap_key_b64: String,
51    pub managed_key_provider_name: String,
52    pub managed_key_reference: String,
53    pub managed_key_b64: String,
54    pub managed_rewrap_key_reference: String,
55    pub managed_rewrap_key_b64: String,
56    pub signing_key_b64: String,
57    pub verifying_key_b64: String,
58    #[serde(default)]
59    pub steps: Vec<IntegrationFullScenarioStep>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63pub struct IntegrationFullRunSummary {
64    pub runner: String,
65    pub scenario: String,
66    pub version: u32,
67    pub tenant_id: String,
68    pub steps: Vec<Value>,
69}
70
71pub fn load_integration_full_manifest(
72    path: impl AsRef<Path>,
73) -> Result<IntegrationFullManifest, SdkError> {
74    let contents = fs::read_to_string(path.as_ref()).map_err(|error| {
75        SdkError::InvalidInput(format!(
76            "failed to read integration-full manifest {}: {error}",
77            path.as_ref().display()
78        ))
79    })?;
80    serde_json::from_str(&contents).map_err(|error| {
81        SdkError::Serialization(format!(
82            "failed to decode integration-full manifest {}: {error}",
83            path.as_ref().display()
84        ))
85    })
86}
87
88fn uses_managed_key_transport(key_transport: Option<&KeyTransportGuidance>) -> bool {
89    !matches!(
90        key_transport.map(|guidance| guidance.mode),
91        None | Some(KeyTransportMode::LocalProvided)
92    )
93}
94
95fn requires_managed_envelope_key(error: &SdkError) -> bool {
96    matches!(
97        error,
98        SdkError::InvalidInput(message)
99            if message.contains(
100                "local envelope protection requires a managed key reference when key_transport.mode is wrapped_key_reference"
101            )
102    )
103}
104
105pub fn run_integration_full_scenario(
106    client: &Client,
107    manifest: &IntegrationFullManifest,
108) -> Result<IntegrationFullRunSummary, SdkError> {
109    let plaintext = manifest.plaintext_utf8.as_bytes();
110    let inline_key =
111        LocalSymmetricKey::new(decode_fixed_32("inline_key_b64", &manifest.inline_key_b64)?);
112    let rewrap_key =
113        LocalSymmetricKey::new(decode_fixed_32("rewrap_key_b64", &manifest.rewrap_key_b64)?);
114    let signing_key = LocalSigningKey::new(decode_fixed_32(
115        "signing_key_b64",
116        &manifest.signing_key_b64,
117    )?);
118    let verifying_key = LocalVerifyingKey::from(decode_fixed_32(
119        "verifying_key_b64",
120        &manifest.verifying_key_b64,
121    )?);
122
123    let workload = manifest.workload.clone();
124    let resource = manifest.resource.clone();
125    let labels = manifest.labels.clone();
126    let attributes = manifest.attributes.clone();
127
128    let envelope_request = LocalProtectionRequest {
129        workload: workload.clone(),
130        resource: resource.clone(),
131        preferred_artifact_profile: Some(ArtifactProfile::Envelope),
132        purpose: Some(manifest.purpose.clone()),
133        labels: labels.clone(),
134        attributes: attributes.clone(),
135    };
136    let tdf_request = LocalProtectionRequest {
137        workload: workload.clone(),
138        resource: resource.clone(),
139        preferred_artifact_profile: Some(ArtifactProfile::Tdf),
140        purpose: Some(manifest.purpose.clone()),
141        labels: labels.clone(),
142        attributes: attributes.clone(),
143    };
144    let detached_signature_request = LocalProtectionRequest {
145        workload: workload.clone(),
146        resource: resource.clone(),
147        preferred_artifact_profile: Some(ArtifactProfile::DetachedSignature),
148        purpose: Some(manifest.purpose.clone()),
149        labels: labels.clone(),
150        attributes: attributes.clone(),
151    };
152
153    let capabilities = client.capabilities()?;
154    let bootstrap = client.bootstrap()?;
155    let whoami = client.whoami()?;
156    let prepared = client.prepare_local_protection(plaintext, envelope_request.clone())?;
157    let cid_binding = client.generate_cid_binding(plaintext, envelope_request.clone())?;
158    let protection_plan = client.protection_plan(&SdkProtectionPlanRequest {
159        operation: ProtectionOperation::Protect,
160        workload: workload.clone(),
161        resource: resource.clone(),
162        preferred_artifact_profile: Some(ArtifactProfile::Tdf),
163        content_digest: Some(manifest.content_digest.clone()),
164        content_size_bytes: Some(plaintext.len() as u64),
165        purpose: Some(manifest.purpose.clone()),
166        labels: labels.clone(),
167        attributes: attributes.clone(),
168    })?;
169    let policy_resolution = client.policy_resolve(&SdkPolicyResolveRequest {
170        operation: ProtectionOperation::Protect,
171        workload: workload.clone(),
172        resource: resource.clone(),
173        content_digest: Some(manifest.content_digest.clone()),
174        content_size_bytes: Some(plaintext.len() as u64),
175        purpose: Some(manifest.purpose.clone()),
176        labels: labels.clone(),
177        attributes: attributes.clone(),
178    })?;
179    let key_access_plan = client.key_access_plan(&SdkKeyAccessPlanRequest {
180        operation: KeyAccessOperation::Wrap,
181        workload: workload.clone(),
182        resource: resource.clone(),
183        artifact_profile: Some(ArtifactProfile::Tdf),
184        key_reference: Some(manifest.managed_key_reference.clone()),
185        content_digest: Some(manifest.content_digest.clone()),
186        purpose: Some(manifest.purpose.clone()),
187        labels: labels.clone(),
188        attributes: attributes.clone(),
189    })?;
190    let envelope_key_access_plan = client.key_access_plan(&SdkKeyAccessPlanRequest {
191        operation: KeyAccessOperation::Wrap,
192        workload: workload.clone(),
193        resource: resource.clone(),
194        artifact_profile: Some(ArtifactProfile::Envelope),
195        key_reference: Some(manifest.managed_key_reference.clone()),
196        content_digest: Some(manifest.content_digest.clone()),
197        purpose: Some(manifest.purpose.clone()),
198        labels: labels.clone(),
199        attributes: attributes.clone(),
200    })?;
201    let mut use_managed_envelope_keys =
202        uses_managed_key_transport(envelope_key_access_plan.execution.key_transport.as_ref());
203
204    let managed_key_source = LocalSymmetricKeySource::managed_reference_with_provider(
205        manifest.managed_key_provider_name.clone(),
206        manifest.managed_key_reference.clone(),
207    );
208    let managed_rewrap_key_source = LocalSymmetricKeySource::managed_reference_with_provider(
209        manifest.managed_key_provider_name.clone(),
210        manifest.managed_rewrap_key_reference.clone(),
211    );
212
213    let signed_detached = client.sign_bytes_with_detached_signature(
214        &signing_key,
215        plaintext,
216        detached_signature_request,
217    )?;
218    let verified_detached = client.verify_bytes_with_detached_signature(
219        &verifying_key,
220        plaintext,
221        &signed_detached.artifact.artifact_bytes,
222    )?;
223
224    let protected_envelope = if use_managed_envelope_keys {
225        client.protect_bytes_with_envelope_using_key_source(
226            &managed_key_source,
227            plaintext,
228            envelope_request,
229        )?
230    } else {
231        match client.protect_bytes_with_envelope(&inline_key, plaintext, envelope_request.clone()) {
232            Ok(result) => result,
233            Err(error) if requires_managed_envelope_key(&error) => {
234                use_managed_envelope_keys = true;
235                client.protect_bytes_with_envelope_using_key_source(
236                    &managed_key_source,
237                    plaintext,
238                    envelope_request,
239                )?
240            }
241            Err(error) => return Err(error),
242        }
243    };
244    let accessed_envelope = if use_managed_envelope_keys {
245        client.access_bytes_with_envelope_using_key_source(
246            &managed_key_source,
247            &protected_envelope.artifact.artifact_bytes,
248        )?
249    } else {
250        client
251            .access_bytes_with_envelope(&inline_key, &protected_envelope.artifact.artifact_bytes)?
252    };
253    let rewrapped_envelope = if use_managed_envelope_keys {
254        client.rewrap_bytes_with_envelope_using_key_sources(
255            &managed_key_source,
256            &managed_rewrap_key_source,
257            &protected_envelope.artifact.artifact_bytes,
258        )?
259    } else {
260        client.rewrap_bytes_with_envelope(
261            &inline_key,
262            &rewrap_key,
263            &protected_envelope.artifact.artifact_bytes,
264        )?
265    };
266
267    let protected_tdf = client.protect_bytes_with_tdf_using_key_source(
268        &managed_key_source,
269        plaintext,
270        tdf_request,
271    )?;
272    let accessed_tdf = client.access_bytes_with_tdf_using_key_source(
273        &managed_key_source,
274        &protected_tdf.artifact.artifact_bytes,
275    )?;
276    let rewrapped_tdf = client.rewrap_bytes_with_tdf_using_key_sources(
277        &managed_key_source,
278        &managed_rewrap_key_source,
279        &protected_tdf.artifact.artifact_bytes,
280    )?;
281    let direct_artifact_registration = client.artifact_register(&SdkArtifactRegisterRequest {
282        operation: ProtectionOperation::Rewrap,
283        workload: workload.clone(),
284        resource: resource.clone(),
285        artifact_profile: ArtifactProfile::Tdf,
286        artifact_digest: rewrapped_tdf.artifact.artifact_digest.clone(),
287        artifact_locator: None,
288        decision_id: None,
289        key_reference: Some(manifest.managed_rewrap_key_reference.clone()),
290        purpose: Some(manifest.purpose.clone()),
291        labels: labels.clone(),
292        attributes: attributes.clone(),
293    })?;
294    let direct_evidence = client.evidence(&SdkEvidenceIngestRequest {
295        event_type: EvidenceEventType::Rewrap,
296        workload,
297        resource,
298        artifact_profile: Some(ArtifactProfile::Tdf),
299        artifact_digest: Some(rewrapped_tdf.artifact.artifact_digest.clone()),
300        decision_id: None,
301        outcome: Some("success".to_string()),
302        occurred_at: None,
303        purpose: Some(manifest.purpose.clone()),
304        labels,
305        attributes,
306    })?;
307
308    Ok(IntegrationFullRunSummary {
309        runner: "sdk-rust".to_string(),
310        scenario: manifest.name.clone(),
311        version: manifest.version,
312        tenant_id: manifest.tenant_id.clone(),
313        steps: vec![
314            integration_full_step(json!({
315                "name": "capabilities",
316                "tenant_id": capabilities.caller.tenant_id,
317                "auth_mode": capabilities.auth_configuration.mode,
318            })),
319            integration_full_step(json!({
320                "name": "bootstrap",
321                "supported_operations": bootstrap.supported_operations,
322            })),
323            integration_full_step(json!({
324                "name": "whoami",
325                "principal_id": whoami.caller.principal_id,
326            })),
327            integration_full_step(json!({
328                "name": "prepare_local_protection",
329                "content_digest": prepared.content_binding.content_digest,
330            })),
331            integration_full_step(json!({
332                "name": "generate_cid_binding",
333                "binding_hash": cid_binding.binding_hash,
334            })),
335            integration_full_step(json!({
336                "name": "protection_plan",
337                "protect_locally": protection_plan.execution.protect_locally,
338            })),
339            integration_full_step(json!({
340                "name": "policy_resolve",
341                "allow": policy_resolution.decision.allow,
342            })),
343            integration_full_step(json!({
344                "name": "key_access_plan",
345                "local_cryptographic_operation": key_access_plan.execution.local_cryptographic_operation,
346            })),
347            integration_full_step(json!({
348                "name": "detached_signature_round_trip",
349                "artifact_digest": signed_detached.artifact.artifact_digest,
350                "signer_key_id": signed_detached.artifact.detached_signature.signer_key_id,
351                "verified_content_digest": verified_detached.content_binding.content_digest,
352            })),
353            integration_full_step(json!({
354                "name": "envelope_round_trip",
355                "artifact_digest": protected_envelope.artifact.artifact_digest,
356                "plaintext_utf8": String::from_utf8_lossy(&accessed_envelope.plaintext).to_string(),
357            })),
358            integration_full_step(json!({
359                "name": "envelope_rewrap",
360                "original_artifact_digest": rewrapped_envelope.original_artifact_digest,
361                "artifact_digest": rewrapped_envelope.artifact.artifact_digest,
362            })),
363            integration_full_step(json!({
364                "name": "tdf_round_trip",
365                "artifact_digest": protected_tdf.artifact.artifact_digest,
366                "plaintext_utf8": String::from_utf8_lossy(&accessed_tdf.plaintext).to_string(),
367            })),
368            integration_full_step(json!({
369                "name": "tdf_rewrap",
370                "original_artifact_digest": rewrapped_tdf.original_artifact_digest,
371                "artifact_digest": rewrapped_tdf.artifact.artifact_digest,
372            })),
373            integration_full_step(json!({
374                "name": "artifact_register_direct",
375                "artifact_digest": direct_artifact_registration.request_summary.artifact_digest,
376                "accepted": direct_artifact_registration.registration.accepted,
377            })),
378            integration_full_step(json!({
379                "name": "evidence_direct",
380                "event_type": direct_evidence.request_summary.event_type,
381                "accepted": direct_evidence.ingestion.accepted,
382            })),
383        ],
384    })
385}
386
387fn integration_full_step(mut step: Value) -> Value {
388    if let Value::Object(ref mut map) = step {
389        map.insert("status".to_string(), Value::String("ok".to_string()));
390    }
391    step
392}
393
394fn decode_fixed_32(label: &str, value: &str) -> Result<[u8; 32], SdkError> {
395    let bytes = BASE64_STANDARD.decode(value).map_err(|error| {
396        SdkError::InvalidInput(format!("failed to decode {label} from base64: {error}"))
397    })?;
398    if bytes.len() != 32 {
399        return Err(SdkError::InvalidInput(format!(
400            "{label} must decode to 32 bytes, got {}",
401            bytes.len()
402        )));
403    }
404    let mut out = [0u8; 32];
405    out.copy_from_slice(&bytes);
406    Ok(out)
407}
408
409#[cfg(test)]
410mod tests {
411    use std::{
412        collections::BTreeMap,
413        env,
414        io::{Read, Write},
415        net::{TcpListener, TcpStream},
416        path::PathBuf,
417        sync::{
418            Arc,
419            atomic::{AtomicBool, Ordering},
420        },
421        thread,
422        time::Duration,
423    };
424
425    use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD};
426    use serde_json::Value;
427    use serde_json::json;
428
429    use crate::{Client, local::LocalSymmetricKey, providers::InMemoryManagedSymmetricKeyProvider};
430
431    use super::{
432        IntegrationFullManifest, load_integration_full_manifest, run_integration_full_scenario,
433    };
434
435    struct ContractServer {
436        base_url: String,
437        address: String,
438        running: Arc<AtomicBool>,
439        handle: Option<thread::JoinHandle<()>>,
440    }
441
442    impl Drop for ContractServer {
443        fn drop(&mut self) {
444            self.running.store(false, Ordering::SeqCst);
445            let _ = TcpStream::connect(&self.address);
446            if let Some(handle) = self.handle.take() {
447                let _ = handle.join();
448            }
449        }
450    }
451
452    #[test]
453    fn loads_integration_full_manifest() {
454        let manifest =
455            load_integration_full_manifest(fixture_manifest_path()).expect("manifest should load");
456        assert_eq!(manifest.name, "integration-full");
457        assert_eq!(manifest.steps.len(), 15);
458    }
459
460    #[test]
461    fn runs_integration_full_scenario_against_contract_server() {
462        let manifest =
463            load_integration_full_manifest(fixture_manifest_path()).expect("manifest should load");
464        let server = spawn_contract_server(manifest.clone());
465
466        let managed_key = decode_test_key(&manifest.managed_key_b64);
467        let managed_rewrap_key = decode_test_key(&manifest.managed_rewrap_key_b64);
468        let mut keys = BTreeMap::new();
469        keys.insert(
470            manifest.managed_key_reference.clone(),
471            LocalSymmetricKey::new(managed_key),
472        );
473        keys.insert(
474            manifest.managed_rewrap_key_reference.clone(),
475            LocalSymmetricKey::new(managed_rewrap_key),
476        );
477
478        let client = Client::builder(server.base_url.clone())
479            .with_managed_symmetric_key_provider(InMemoryManagedSymmetricKeyProvider::new(
480                manifest.managed_key_provider_name.clone(),
481                keys,
482            ))
483            .build()
484            .expect("client should build");
485
486        let summary =
487            run_integration_full_scenario(&client, &manifest).expect("runner should succeed");
488
489        assert_eq!(summary.runner, "sdk-rust");
490        assert_eq!(summary.steps.len(), 15);
491        assert_eq!(summary.steps[8]["name"], "detached_signature_round_trip");
492        assert_eq!(summary.steps[12]["name"], "tdf_rewrap");
493        assert_eq!(summary.steps[13]["accepted"], true);
494        assert_eq!(summary.steps[14]["accepted"], true);
495    }
496
497    fn fixture_manifest_path() -> PathBuf {
498        if let Ok(configured) = env::var("SDK_API_E2E_INTEGRATION_FULL_MANIFEST_PATH") {
499            let configured = configured.trim();
500            if !configured.is_empty() {
501                return PathBuf::from(configured);
502            }
503        }
504
505        let base = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
506        let candidates = [
507            base.join("..")
508                .join("prop-system-tests")
509                .join("fixtures")
510                .join("sdk_api_e2e")
511                .join("integration_full_manifest.json"),
512            base.join("prop-system-tests")
513                .join("fixtures")
514                .join("sdk_api_e2e")
515                .join("integration_full_manifest.json"),
516        ];
517
518        for candidate in candidates {
519            if candidate.exists() {
520                return candidate;
521            }
522        }
523
524        panic!(
525            "integration-full manifest not found; set SDK_API_E2E_INTEGRATION_FULL_MANIFEST_PATH"
526        )
527    }
528
529    fn decode_test_key(value: &str) -> [u8; 32] {
530        let bytes = BASE64_STANDARD.decode(value).expect("valid test key");
531        let mut out = [0u8; 32];
532        out.copy_from_slice(&bytes);
533        out
534    }
535
536    fn spawn_contract_server(manifest: IntegrationFullManifest) -> ContractServer {
537        let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind");
538        listener
539            .set_nonblocking(true)
540            .expect("listener should be nonblocking");
541        let address = listener.local_addr().expect("listener addr").to_string();
542        let base_url = format!("http://{address}");
543        let running = Arc::new(AtomicBool::new(true));
544        let running_clone = Arc::clone(&running);
545
546        let handle = thread::spawn(move || {
547            while running_clone.load(Ordering::SeqCst) {
548                match listener.accept() {
549                    Ok((mut stream, _)) => {
550                        let _ = stream.set_nonblocking(false);
551                        let request = match read_http_request(&mut stream) {
552                            Ok(request) => request,
553                            Err(_) => continue,
554                        };
555                        let response_body = response_json(&manifest, &request.path, &request.body);
556                        let response = format!(
557                            "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
558                            response_body.len(),
559                            response_body
560                        );
561                        let _ = stream.write_all(response.as_bytes());
562                        let _ = stream.flush();
563                    }
564                    Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
565                        thread::sleep(Duration::from_millis(10));
566                    }
567                    Err(_) => break,
568                }
569            }
570        });
571
572        ContractServer {
573            base_url,
574            address,
575            running,
576            handle: Some(handle),
577        }
578    }
579
580    struct HttpRequest {
581        path: String,
582        body: String,
583    }
584
585    fn read_http_request(stream: &mut TcpStream) -> std::io::Result<HttpRequest> {
586        stream.set_read_timeout(Some(Duration::from_secs(1)))?;
587
588        let mut buffer = Vec::new();
589        let mut chunk = [0u8; 4096];
590
591        loop {
592            let read = stream.read(&mut chunk)?;
593            if read == 0 {
594                break;
595            }
596            buffer.extend_from_slice(&chunk[..read]);
597            if let Some(header_end) = find_header_end(&buffer) {
598                let content_length = parse_content_length(&buffer[..header_end]);
599                let body_len = buffer.len().saturating_sub(header_end + 4);
600                if body_len >= content_length {
601                    break;
602                }
603            }
604        }
605
606        let request = String::from_utf8_lossy(&buffer).to_string();
607        let (head, body) = request.split_once("\r\n\r\n").unwrap_or((&request, ""));
608        let request_line = head.lines().next().unwrap_or("");
609        let path = request_line
610            .split_whitespace()
611            .nth(1)
612            .unwrap_or("/")
613            .to_string();
614        Ok(HttpRequest {
615            path,
616            body: body.to_string(),
617        })
618    }
619
620    fn find_header_end(buffer: &[u8]) -> Option<usize> {
621        buffer.windows(4).position(|window| window == b"\r\n\r\n")
622    }
623
624    fn parse_content_length(headers: &[u8]) -> usize {
625        let text = String::from_utf8_lossy(headers);
626        text.lines()
627            .find_map(|line| {
628                let (name, value) = line.split_once(':')?;
629                if name.eq_ignore_ascii_case("content-length") {
630                    value.trim().parse::<usize>().ok()
631                } else {
632                    None
633                }
634            })
635            .unwrap_or(0)
636    }
637
638    fn response_json(manifest: &IntegrationFullManifest, path: &str, body: &str) -> String {
639        match path {
640            "/v1/sdk/capabilities" => json!({
641                "service": "lattix-platform-api",
642                "status": "ready",
643                "auth_mode": "bearer_token",
644                "auth_configuration": auth_configuration(),
645                "caller": caller(manifest),
646                "default_required_scopes": manifest.default_required_scopes,
647                "routes": [],
648            })
649            .to_string(),
650            "/v1/sdk/bootstrap" => json!({
651                "service": "lattix-platform-api",
652                "status": "ready",
653                "auth_mode": "bearer_token",
654                "auth_configuration": auth_configuration(),
655                "caller": caller(manifest),
656                "enforcement_model": "embedded_local_library",
657                "plaintext_to_platform": false,
658                "policy_resolution_mode": "metadata_only_control_plane",
659                "supported_operations": ["protect", "access", "rewrap"],
660                "supported_artifact_profiles": ["tdf", "envelope", "detached_signature"],
661                "platform_domains": [{"domain": "policy", "configured": true, "reason": "metadata-only"}],
662            })
663            .to_string(),
664            "/v1/sdk/whoami" => json!({
665                "service": "lattix-platform-api",
666                "status": "ok",
667                "caller": caller(manifest),
668            })
669            .to_string(),
670            "/v1/sdk/policy-resolve" => json!(policy_response(manifest, request_operation(body)))
671                .to_string(),
672            "/v1/sdk/protection-plan" => {
673                json!(protection_plan_response(manifest, request_operation(body), request_profile(body)))
674                    .to_string()
675            }
676            "/v1/sdk/key-access-plan" => {
677                json!(key_access_plan_response(manifest, request_operation(body), request_profile(body)))
678                    .to_string()
679            }
680            "/v1/sdk/artifact-register" => {
681                json!(artifact_register_response(manifest, request_operation(body), request_profile(body), request_artifact_digest(body)))
682                    .to_string()
683            }
684            "/v1/sdk/evidence" => {
685                json!(evidence_response(manifest, request_event_type(body), request_profile(body)))
686                    .to_string()
687            }
688            _ => json!({"status": "unexpected", "path": path}).to_string(),
689        }
690    }
691
692    fn auth_configuration() -> Value {
693        json!({
694            "mode": "oauth_client_credentials",
695            "proof_of_possession": "mtls",
696            "oidc_issuer": "https://issuer.example",
697            "oidc_audience": "lattix-platform-api",
698            "oidc_issuer_ready": true,
699            "mtls_ready": true,
700        })
701    }
702
703    fn caller(manifest: &IntegrationFullManifest) -> Value {
704        json!({
705            "tenant_id": manifest.tenant_id,
706            "principal_id": manifest.principal_id,
707            "subject": manifest.subject,
708            "auth_source": "bearer_token",
709            "scopes": manifest.default_required_scopes,
710        })
711    }
712
713    fn policy_response(manifest: &IntegrationFullManifest, operation: &str) -> Value {
714        json!({
715            "service": "lattix-platform-api",
716            "status": "ready",
717            "caller": caller(manifest),
718            "request_summary": {
719                "operation": operation,
720                "workload_application": manifest.workload.application,
721                "workload_environment": manifest.workload.environment,
722                "workload_component": manifest.workload.component,
723                "resource_kind": manifest.resource.kind,
724                "resource_id": manifest.resource.id,
725                "mime_type": manifest.resource.mime_type,
726                "content_digest_present": true,
727                "content_size_bytes": manifest.plaintext_utf8.len(),
728                "purpose": manifest.purpose,
729                "label_count": manifest.labels.len(),
730                "attribute_count": manifest.attributes.len(),
731            },
732            "decision": {
733                "allow": true,
734                "enforcement_mode": "local_embedded_enforcement",
735                "required_scopes": [],
736                "policy_inputs": [],
737                "required_actions": [],
738            },
739            "handling": {
740                "protect_locally": true,
741                "plaintext_transport": "forbidden_by_default",
742                "bind_policy_to": ["artifact_digest", "content_digest"],
743                "evidence_expected": [],
744            },
745            "platform_domains": [],
746            "warnings": [],
747        })
748    }
749
750    fn protection_plan_response(
751        manifest: &IntegrationFullManifest,
752        operation: &str,
753        profile: &str,
754    ) -> Value {
755        let key_transport = if profile == "tdf" {
756            json!({
757                "mode": "wrapped_key_reference",
758                "key_material_origin": "kms",
759                "stable_key_reference_preferred": true,
760                "raw_key_delivery_forbidden": true,
761            })
762        } else {
763            Value::Null
764        };
765        json!({
766            "service": "lattix-platform-api",
767            "status": "ready",
768            "caller": caller(manifest),
769            "request_summary": {
770                "operation": operation,
771                "workload_application": manifest.workload.application,
772                "workload_environment": manifest.workload.environment,
773                "workload_component": manifest.workload.component,
774                "resource_kind": manifest.resource.kind,
775                "resource_id": manifest.resource.id,
776                "mime_type": manifest.resource.mime_type,
777                "preferred_artifact_profile": profile,
778                "content_digest_present": true,
779                "content_size_bytes": manifest.plaintext_utf8.len(),
780                "label_count": manifest.labels.len(),
781                "attribute_count": manifest.attributes.len(),
782                "purpose": manifest.purpose,
783            },
784            "decision": {
785                "allow": true,
786                "required_scopes": [],
787                "handling_mode": "local_embedded_enforcement",
788                "plaintext_transport": "forbidden_by_default",
789            },
790            "execution": {
791                "protect_locally": true,
792                "local_enforcement_library": "sdk_embedded_library",
793                "send_plaintext_to_platform": false,
794                "send_only": ["content digest"],
795                "artifact_profile": profile,
796                "key_strategy": "local",
797                "policy_resolution": "metadata_only",
798                "key_transport": key_transport,
799            },
800            "platform_domains": [],
801            "warnings": [],
802        })
803    }
804
805    fn key_access_plan_response(
806        manifest: &IntegrationFullManifest,
807        operation: &str,
808        profile: &str,
809    ) -> Value {
810        let key_reference_present = profile == "tdf" || profile == "detached_signature";
811        let key_transport = if profile == "tdf" {
812            json!({
813                "mode": "wrapped_key_reference",
814                "key_material_origin": "kms",
815                "stable_key_reference_preferred": true,
816                "raw_key_delivery_forbidden": true,
817            })
818        } else {
819            Value::Null
820        };
821        json!({
822            "service": "lattix-platform-api",
823            "status": "ready",
824            "caller": caller(manifest),
825            "request_summary": {
826                "operation": operation,
827                "workload_application": manifest.workload.application,
828                "workload_environment": manifest.workload.environment,
829                "workload_component": manifest.workload.component,
830                "resource_kind": manifest.resource.kind,
831                "resource_id": manifest.resource.id,
832                "mime_type": manifest.resource.mime_type,
833                "artifact_profile": profile,
834                "key_reference_present": key_reference_present,
835                "content_digest_present": true,
836                "purpose": manifest.purpose,
837                "label_count": manifest.labels.len(),
838                "attribute_count": manifest.attributes.len(),
839            },
840            "decision": {
841                "allow": true,
842                "required_scopes": [],
843                "operation": operation,
844                "key_reference_present": key_reference_present,
845            },
846            "execution": {
847                "local_cryptographic_operation": true,
848                "platform_role": "authorize only",
849                "send_plaintext_to_platform": false,
850                "send_only": ["content digest"],
851                "artifact_profile": profile,
852                "authorization_strategy": "metadata_only",
853                "key_transport": key_transport,
854            },
855            "platform_domains": [],
856            "warnings": [],
857        })
858    }
859
860    fn artifact_register_response(
861        manifest: &IntegrationFullManifest,
862        operation: &str,
863        profile: &str,
864        artifact_digest: &str,
865    ) -> Value {
866        json!({
867            "service": "lattix-platform-api",
868            "status": "ready",
869            "caller": caller(manifest),
870            "request_summary": {
871                "operation": operation,
872                "workload_application": manifest.workload.application,
873                "workload_environment": manifest.workload.environment,
874                "workload_component": manifest.workload.component,
875                "resource_kind": manifest.resource.kind,
876                "resource_id": manifest.resource.id,
877                "mime_type": manifest.resource.mime_type,
878                "artifact_profile": profile,
879                "artifact_digest": artifact_digest,
880                "artifact_locator_present": false,
881                "decision_id_present": false,
882                "key_reference_present": profile == "tdf",
883                "purpose": manifest.purpose,
884                "label_count": manifest.labels.len(),
885                "attribute_count": manifest.attributes.len(),
886            },
887            "registration": {
888                "accepted": true,
889                "required_scopes": [],
890                "artifact_transport": "metadata_only",
891                "send_plaintext_to_platform": false,
892                "catalog_actions": [],
893                "evidence_expected": [],
894            },
895            "platform_domains": [],
896            "warnings": [],
897        })
898    }
899
900    fn evidence_response(
901        manifest: &IntegrationFullManifest,
902        event_type: &str,
903        profile: &str,
904    ) -> Value {
905        json!({
906            "service": "lattix-platform-api",
907            "status": "ready",
908            "caller": caller(manifest),
909            "request_summary": {
910                "event_type": event_type,
911                "workload_application": manifest.workload.application,
912                "workload_environment": manifest.workload.environment,
913                "workload_component": manifest.workload.component,
914                "resource_kind": manifest.resource.kind,
915                "resource_id": manifest.resource.id,
916                "mime_type": manifest.resource.mime_type,
917                "artifact_profile": profile,
918                "artifact_digest_present": true,
919                "decision_id_present": false,
920                "outcome": "success",
921                "occurred_at": null,
922                "purpose": manifest.purpose,
923                "label_count": manifest.labels.len(),
924                "attribute_count": manifest.attributes.len(),
925            },
926            "ingestion": {
927                "accepted": true,
928                "required_scopes": [],
929                "plaintext_transport": "forbidden_by_default",
930                "send_only": [],
931                "correlate_by": [],
932            },
933            "platform_domains": [],
934            "warnings": [],
935        })
936    }
937
938    fn request_operation(body: &str) -> &str {
939        if body.contains("\"operation\":\"unwrap\"") {
940            "unwrap"
941        } else if body.contains("\"operation\":\"wrap\"") {
942            "wrap"
943        } else if body.contains("\"operation\":\"access\"") {
944            "access"
945        } else if body.contains("\"operation\":\"rewrap\"") {
946            "rewrap"
947        } else {
948            "protect"
949        }
950    }
951
952    fn request_profile(body: &str) -> &str {
953        if body.contains("\"detached_signature\"") {
954            "detached_signature"
955        } else if body.contains("\"envelope\"") {
956            "envelope"
957        } else {
958            "tdf"
959        }
960    }
961
962    fn request_artifact_digest(body: &str) -> &str {
963        const PREFIX: &str = "\"artifact_digest\":\"";
964        extract_json_string(body, PREFIX).unwrap_or("sha256:artifact-tdf-rewrapped")
965    }
966
967    fn request_event_type(body: &str) -> &str {
968        const PREFIX: &str = "\"event_type\":\"";
969        extract_json_string(body, PREFIX).unwrap_or("rewrap")
970    }
971
972    fn extract_json_string<'a>(body: &'a str, prefix: &str) -> Option<&'a str> {
973        let start = body.find(prefix)? + prefix.len();
974        let end = body[start..].find('"')? + start;
975        Some(&body[start..end])
976    }
977}