Skip to main content

venice_e2ee_proxy/
attestation.rs

1//! Attestation fetch, verification policy, and fail-closed checks.
2//!
3//! This module intentionally does not cache attestation results internally.
4//! Attestation/model-key state is tied to the session lifetime, so callers should
5//! store a successful [`VerifiedAttestation`] in the session manager only for
6//! that session's TTL/request budget. Calling
7//! [`AttestationVerifier::verify_model_attestation`] always generates a fresh
8//! nonce and fetches fresh Venice evidence.
9//!
10//! v0.1 deliberately does not implement measurement allowlists for TDX RTMR/MRTD
11//! or NVIDIA claims. It verifies the basic Venice attestation envelope, performs
12//! local key/address validation, enforces debug-mode policy where evidence exposes
13//! it, and exposes strict fail-closed gates for required TDX/NRAS verification.
14//! Full DCAP/QVL and NRAS cryptographic verification is not linked; when those
15//! verifiers are required by policy, verification fails closed with
16//! [`AttestationError::ExternalVerifierUnavailable`].
17
18use std::{fmt, time::SystemTime};
19
20use k256::{PublicKey, elliptic_curve::sec1::ToEncodedPoint};
21use rand_core::{OsRng, RngCore};
22use serde::Deserialize;
23use serde_json::Value;
24use sha2::{Digest as Sha2Digest, Sha256};
25use sha3::Keccak256;
26use thiserror::Error;
27
28use crate::{
29    config::{AttestationConfig, NvidiaRequirement, ProxyConfig},
30    venice::{VeniceClient, VeniceClientError},
31};
32
33const ATTESTATION_NONCE_BYTES: usize = 32;
34const ATTESTATION_NONCE_HEX_CHARS: usize = ATTESTATION_NONCE_BYTES * 2;
35const TDX_TEE_TYPE: u32 = 0x81;
36const TDX_QUOTE_HEADER_LEN: usize = 48;
37const TDX_QUOTE_TEE_TYPE_OFFSET: usize = 4;
38const TDX_QUOTE_TEE_TYPE_END: usize = TDX_QUOTE_TEE_TYPE_OFFSET + 4;
39const TDX_REPORT_BODY_OFFSET: usize = TDX_QUOTE_HEADER_LEN;
40const TDX_REPORT_TD_ATTRIBUTES_OFFSET: usize = TDX_REPORT_BODY_OFFSET + 120;
41const TDX_REPORT_TD_ATTRIBUTES_END: usize = TDX_REPORT_TD_ATTRIBUTES_OFFSET + 8;
42const TDX_REPORT_DATA_OFFSET: usize = TDX_REPORT_BODY_OFFSET + 520;
43const TDX_REPORT_DATA_LEN: usize = 64;
44const TDX_REPORT_DATA_END: usize = TDX_REPORT_DATA_OFFSET + TDX_REPORT_DATA_LEN;
45
46/// Verifies Venice model attestation evidence according to the configured policy.
47#[derive(Clone, Debug)]
48pub struct AttestationVerifier {
49    policy: AttestationConfig,
50    venice_client: VeniceClient,
51}
52
53/// Fresh random nonce sent to Venice and checked against attestation evidence.
54#[derive(Clone, PartialEq, Eq)]
55pub struct AttestationNonce(String);
56
57/// Successful attestation result cached with a session and exposed through proxy metadata.
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub struct VerifiedAttestation {
60    pub model_id: String,
61    pub model_public_key: String,
62    pub signing_address: Option<String>,
63    pub tee_provider: Option<String>,
64    pub debug: Option<bool>,
65    pub tdx: TdxVerificationSummary,
66    pub nvidia: NvidiaVerificationSummary,
67    pub verified_at: SystemTime,
68}
69
70/// Summary of TDX evidence presence, local checks, and debug status.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct TdxVerificationSummary {
73    pub present: bool,
74    pub verified: bool,
75    pub debug: Option<bool>,
76    pub tee_type: Option<u32>,
77}
78
79/// Summary of NVIDIA attestation evidence presence and verification status.
80#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct NvidiaVerificationSummary {
82    pub present: bool,
83    pub verified: NvidiaVerificationStatus,
84}
85
86/// Verification status for NVIDIA attestation evidence under the configured policy.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum NvidiaVerificationStatus {
89    NotPresent,
90    IgnoredByPolicy,
91    PresentVerifierUnavailable,
92}
93
94/// Errors returned while fetching or validating attestation evidence.
95#[derive(Debug, Error)]
96pub enum AttestationError {
97    #[error("invalid attestation request: {message}")]
98    InvalidRequest { message: String },
99    #[error("TEE attestation fetch failed: {0}")]
100    Fetch(#[from] VeniceClientError),
101    #[error("TEE attestation response is malformed: {message}")]
102    MalformedResponse { message: String },
103    #[error("TEE attestation evidence is missing required field {field}")]
104    MissingField { field: &'static str },
105    #[error("TEE attestation verification failed: {message}")]
106    PolicyViolation {
107        code: AttestationFailureCode,
108        message: String,
109    },
110    #[error("TEE attestation verifier unavailable: {message}")]
111    ExternalVerifierUnavailable {
112        verifier: &'static str,
113        message: String,
114    },
115}
116
117/// Stable failure codes for attestation policy violations.
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum AttestationFailureCode {
120    UpstreamNotVerified,
121    NonceMismatch,
122    ModelMismatch,
123    InvalidSigningKey,
124    SigningAddressMismatch,
125    DebugModeDetected,
126    MissingTdxEvidence,
127    InvalidTdxEvidence,
128    MissingNvidiaEvidence,
129    InvalidNvidiaEvidence,
130}
131
132/// TDX quote fields used by local policy checks.
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134struct ParsedTdxQuote {
135    tee_type: u32,
136    debug: bool,
137}
138
139/// Typed Venice ACI attestation response used by the production E2EE endpoint.
140#[derive(Debug, Clone, Deserialize)]
141struct VeniceAttestationResponse {
142    attestation: AciAttestationEnvelope,
143    #[serde(flatten)]
144    fields: VeniceAttestationFields,
145}
146
147/// Root Venice attestation fields containing verification decision and model key binding.
148#[derive(Debug, Clone, Default, Deserialize)]
149struct VeniceAttestationFields {
150    #[serde(default)]
151    verified: Option<bool>,
152    #[serde(default)]
153    nonce: Option<String>,
154    #[serde(default)]
155    model: Option<String>,
156    #[serde(default)]
157    tee_provider: Option<String>,
158    #[serde(default)]
159    signing_public_key: Option<String>,
160    #[serde(default)]
161    signing_address: Option<String>,
162    #[serde(default)]
163    debug: Option<bool>,
164    #[serde(default)]
165    nvidia_payload: Option<Value>,
166}
167
168/// ACI nested attestation object containing hardware evidence.
169#[derive(Debug, Clone, Deserialize)]
170struct AciAttestationEnvelope {
171    #[serde(default)]
172    evidence: AciEvidenceFields,
173}
174
175/// ACI nested hardware evidence fields used by the local verifier.
176#[derive(Debug, Clone, Default, Deserialize)]
177struct AciEvidenceFields {
178    #[serde(default)]
179    quote: Option<String>,
180    #[serde(default)]
181    quote_report_data: Option<String>,
182}
183
184impl AttestationVerifier {
185    /// Builds a verifier from proxy configuration and the Venice client used to fetch evidence.
186    pub fn from_config(config: &ProxyConfig, venice_client: VeniceClient) -> Self {
187        Self::new(config.attestation.clone(), venice_client)
188    }
189
190    /// Builds a verifier from an attestation policy and Venice client.
191    pub fn new(policy: AttestationConfig, venice_client: VeniceClient) -> Self {
192        Self {
193            policy,
194            venice_client,
195        }
196    }
197
198    /// Returns the attestation policy used by this verifier.
199    pub fn policy(&self) -> &AttestationConfig {
200        &self.policy
201    }
202
203    /// Fetches Venice attestation evidence with a fresh nonce and verifies it
204    /// according to the configured fail-closed policy.
205    pub async fn verify_model_attestation(
206        &self,
207        model_id: &str,
208    ) -> Result<VerifiedAttestation, AttestationError> {
209        if model_id.trim().is_empty() {
210            return Err(AttestationError::InvalidRequest {
211                message: "model id must not be empty".to_owned(),
212            });
213        }
214
215        let nonce = AttestationNonce::generate();
216        let evidence = self
217            .venice_client
218            .fetch_attestation_evidence(model_id, nonce.as_str())
219            .await
220            .map_err(AttestationError::Fetch)?;
221
222        self.verify_evidence(model_id, nonce.as_str(), evidence)
223    }
224
225    /// Verifies already-fetched evidence for a requested model and nonce.
226    pub fn verify_evidence(
227        &self,
228        requested_model_id: &str,
229        client_nonce: &str,
230        upstream_response: Value,
231    ) -> Result<VerifiedAttestation, AttestationError> {
232        verify_attestation_evidence(
233            &self.policy,
234            requested_model_id,
235            client_nonce,
236            upstream_response,
237        )
238    }
239}
240
241impl AttestationNonce {
242    /// Generates a 32-byte nonce encoded as lowercase hex.
243    pub fn generate() -> Self {
244        let mut bytes = [0_u8; ATTESTATION_NONCE_BYTES];
245        OsRng.fill_bytes(&mut bytes);
246        Self(hex::encode(bytes))
247    }
248
249    /// Returns the nonce as a hex string slice.
250    pub fn as_str(&self) -> &str {
251        &self.0
252    }
253}
254
255impl fmt::Debug for AttestationNonce {
256    /// Formats the nonce for diagnostics.
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        f.debug_tuple("AttestationNonce").field(&self.0).finish()
259    }
260}
261
262impl TdxVerificationSummary {
263    /// Returns a summary representing absent TDX evidence.
264    fn not_present() -> Self {
265        Self {
266            present: false,
267            verified: false,
268            debug: None,
269            tee_type: None,
270        }
271    }
272}
273
274impl NvidiaVerificationSummary {
275    /// Returns a summary representing absent NVIDIA evidence.
276    fn not_present() -> Self {
277        Self {
278            present: false,
279            verified: NvidiaVerificationStatus::NotPresent,
280        }
281    }
282}
283
284impl NvidiaVerificationStatus {
285    /// Returns the metadata header value for this NVIDIA verification status.
286    pub fn as_header_value(self) -> &'static str {
287        match self {
288            Self::NotPresent => "not-present",
289            Self::IgnoredByPolicy => "ignored",
290            Self::PresentVerifierUnavailable => "verifier-unavailable",
291        }
292    }
293}
294
295impl AttestationError {
296    /// Returns the OpenAI-compatible error type exposed for this attestation error.
297    pub fn api_error_type(&self) -> &'static str {
298        match self {
299            Self::InvalidRequest { .. } => "invalid_request_error",
300            Self::ExternalVerifierUnavailable { .. } => "proxy_attestation_verifier_unavailable",
301            Self::Fetch(_)
302            | Self::MalformedResponse { .. }
303            | Self::MissingField { .. }
304            | Self::PolicyViolation { .. } => "proxy_attestation_error",
305        }
306    }
307
308    /// Returns the proxy error code exposed for this attestation error.
309    pub fn api_error_code(&self) -> &'static str {
310        match self {
311            Self::InvalidRequest { .. } => "invalid_attestation_request",
312            Self::Fetch(_) => "attestation_fetch_failed",
313            Self::MalformedResponse { .. } => "attestation_malformed_response",
314            Self::MissingField { .. } => "attestation_missing_required_field",
315            Self::PolicyViolation { code, .. } => code.as_str(),
316            Self::ExternalVerifierUnavailable { .. } => "attestation_verifier_unavailable",
317        }
318    }
319
320    /// Returns whether the error indicates a required external attestation verifier is unavailable.
321    pub fn verifier_unavailable(&self) -> bool {
322        matches!(self, Self::ExternalVerifierUnavailable { .. })
323    }
324}
325
326impl AttestationFailureCode {
327    /// Returns the stable string form used in proxy error responses.
328    pub fn as_str(self) -> &'static str {
329        match self {
330            Self::UpstreamNotVerified => "attestation_upstream_not_verified",
331            Self::NonceMismatch => "attestation_nonce_mismatch",
332            Self::ModelMismatch => "attestation_model_mismatch",
333            Self::InvalidSigningKey => "attestation_invalid_signing_key",
334            Self::SigningAddressMismatch => "attestation_signing_address_mismatch",
335            Self::DebugModeDetected => "attestation_debug_mode_detected",
336            Self::MissingTdxEvidence => "attestation_missing_tdx_evidence",
337            Self::InvalidTdxEvidence => "attestation_invalid_tdx_evidence",
338            Self::MissingNvidiaEvidence => "attestation_missing_nvidia_evidence",
339            Self::InvalidNvidiaEvidence => "attestation_invalid_nvidia_evidence",
340        }
341    }
342}
343
344impl VeniceAttestationResponse {
345    /// Parses the raw Venice payload into the supported attestation response model.
346    fn parse(value: Value) -> Result<Self, AttestationError> {
347        serde_json::from_value(value).map_err(|source| AttestationError::MalformedResponse {
348            message: source.to_string(),
349        })
350    }
351
352    /// Returns the root fields containing Venice's verification decision and model key binding.
353    fn fields(&self) -> &VeniceAttestationFields {
354        &self.fields
355    }
356
357    /// Returns the TDX quote from ACI hardware evidence.
358    fn tdx_quote(&self) -> Option<&str> {
359        non_empty(self.attestation.evidence.quote.as_deref())
360    }
361
362    /// Returns TDX report data from ACI hardware evidence.
363    fn quote_report_data(&self) -> Option<&str> {
364        non_empty(self.attestation.evidence.quote_report_data.as_deref())
365    }
366}
367
368impl VeniceAttestationFields {
369    /// Reads Venice's required `verified` decision.
370    fn required_verified(&self) -> Result<bool, AttestationError> {
371        self.verified
372            .ok_or(AttestationError::MissingField { field: "verified" })
373    }
374
375    /// Reads a required non-empty string field from this attestation model.
376    fn required_string<'a>(
377        &'a self,
378        field: &'static str,
379        value: Option<&'a str>,
380    ) -> Result<&'a str, AttestationError> {
381        match value {
382            Some(value) if !value.trim().is_empty() => Ok(value),
383            Some(_) => Err(AttestationError::MalformedResponse {
384                message: format!("field {field} must not be empty"),
385            }),
386            None => Err(AttestationError::MissingField { field }),
387        }
388    }
389
390    fn nonce(&self) -> Option<&str> {
391        self.nonce.as_deref()
392    }
393
394    fn model(&self) -> Option<&str> {
395        self.model.as_deref()
396    }
397
398    fn tee_provider(&self) -> Option<&str> {
399        non_empty(self.tee_provider.as_deref())
400    }
401
402    fn signing_public_key(&self) -> Option<&str> {
403        non_empty(self.signing_public_key.as_deref())
404    }
405
406    fn signing_address(&self) -> Option<&str> {
407        non_empty(self.signing_address.as_deref())
408    }
409
410    fn debug(&self) -> Option<bool> {
411        self.debug
412    }
413
414    fn nvidia_payload(&self) -> Option<&Value> {
415        self.nvidia_payload
416            .as_ref()
417            .filter(|value| !value.is_null())
418    }
419}
420
421/// Validates a Venice attestation response against the expected model, nonce, and policy.
422fn verify_attestation_evidence(
423    policy: &AttestationConfig,
424    requested_model_id: &str,
425    client_nonce: &str,
426    upstream_response: Value,
427) -> Result<VerifiedAttestation, AttestationError> {
428    validate_nonce_hex(client_nonce)?;
429
430    let response_model = VeniceAttestationResponse::parse(upstream_response)?;
431    let evidence = response_model.fields();
432    let verified = evidence.required_verified()?;
433
434    if !verified {
435        return policy_error(
436            AttestationFailureCode::UpstreamNotVerified,
437            "Venice did not mark the attestation evidence as verified",
438        );
439    }
440
441    let nonce = evidence.required_string("nonce", evidence.nonce())?;
442
443    if nonce != client_nonce {
444        return policy_error(
445            AttestationFailureCode::NonceMismatch,
446            "attestation nonce does not match the client nonce; evidence may be stale or replayed",
447        );
448    }
449
450    let model = evidence.required_string("model", evidence.model())?;
451
452    if model != requested_model_id {
453        return policy_error(
454            AttestationFailureCode::ModelMismatch,
455            format!(
456                "attestation model {model:?} does not match requested model {requested_model_id:?}"
457            ),
458        );
459    }
460
461    let signing_key = evidence
462        .signing_public_key()
463        .ok_or(AttestationError::MissingField {
464            field: "signing_public_key",
465        })?;
466    let normalized_signing_key = normalize_public_key_hex(signing_key)?;
467    let derived_address = ethereum_address_from_uncompressed_key_hex(&normalized_signing_key)?;
468    let signing_address = evidence
469        .signing_address()
470        .map(normalize_ethereum_address)
471        .transpose()?;
472
473    if let Some(signing_address) = &signing_address
474        && signing_address != &derived_address
475    {
476        return policy_error(
477            AttestationFailureCode::SigningAddressMismatch,
478            format!(
479                "signing_address {signing_address} does not match address {derived_address} derived from signing key"
480            ),
481        );
482    }
483
484    let debug = evidence.debug();
485
486    if debug == Some(true) && !policy.allow_debug {
487        return policy_error(
488            AttestationFailureCode::DebugModeDetected,
489            "attestation evidence reports debug mode and attestation.allow_debug=false",
490        );
491    }
492
493    let tdx = evaluate_tdx_policy(
494        policy,
495        &response_model,
496        &normalized_signing_key,
497        signing_address.as_deref(),
498    )?;
499    let nvidia = evaluate_nvidia_policy(policy, evidence)?;
500
501    Ok(VerifiedAttestation {
502        model_id: requested_model_id.to_owned(),
503        model_public_key: normalized_signing_key,
504        signing_address,
505        tee_provider: evidence.tee_provider().map(ToOwned::to_owned),
506        debug,
507        tdx,
508        nvidia,
509        verified_at: SystemTime::now(),
510    })
511}
512
513/// Evaluates TDX evidence fields against the configured TDX policy.
514fn evaluate_tdx_policy(
515    policy: &AttestationConfig,
516    response: &VeniceAttestationResponse,
517    signing_key: &str,
518    signing_address: Option<&str>,
519) -> Result<TdxVerificationSummary, AttestationError> {
520    let Some(tdx_quote) = response.tdx_quote() else {
521        return if policy.require_tdx {
522            policy_error(
523                AttestationFailureCode::MissingTdxEvidence,
524                "attestation.require_tdx=true but attestation.evidence.quote is absent",
525            )
526        } else {
527            Ok(TdxVerificationSummary::not_present())
528        };
529    };
530
531    let parsed = parse_tdx_quote(tdx_quote)?;
532
533    if parsed.tee_type != TDX_TEE_TYPE {
534        return policy_error(
535            AttestationFailureCode::InvalidTdxEvidence,
536            format!(
537                "Intel quote teeType 0x{:x} is not TDX teeType 0x{TDX_TEE_TYPE:x}",
538                parsed.tee_type
539            ),
540        );
541    }
542
543    if parsed.debug && !policy.allow_debug {
544        return policy_error(
545            AttestationFailureCode::DebugModeDetected,
546            "Intel TDX quote reports debug mode and attestation.allow_debug=false",
547        );
548    }
549
550    if let Some(reportdata) = response.quote_report_data() {
551        verify_reportdata_binding(reportdata, signing_key, signing_address)?;
552    }
553
554    if policy.require_tdx {
555        let message = if policy.pccs_url.trim().is_empty() {
556            "attestation.require_tdx=true requires independent DCAP/QVL quote verification, but no DCAP verifier is linked and attestation.pccs_url is empty".to_owned()
557        } else {
558            "attestation.require_tdx=true requires independent DCAP/QVL quote verification; PCCS URL is configured but this v0.1 verifier has no DCAP/QVL backend linked".to_owned()
559        };
560
561        return Err(AttestationError::ExternalVerifierUnavailable {
562            verifier: "tdx-dcap-qvl",
563            message,
564        });
565    }
566
567    Ok(TdxVerificationSummary {
568        present: true,
569        verified: false,
570        debug: Some(parsed.debug),
571        tee_type: Some(parsed.tee_type),
572    })
573}
574
575/// Evaluates NVIDIA evidence fields against the configured NVIDIA policy.
576fn evaluate_nvidia_policy(
577    policy: &AttestationConfig,
578    evidence: &VeniceAttestationFields,
579) -> Result<NvidiaVerificationSummary, AttestationError> {
580    let nvidia_payload = evidence.nvidia_payload();
581
582    match (policy.require_nvidia, nvidia_payload) {
583        (NvidiaRequirement::Required, None) => policy_error(
584            AttestationFailureCode::MissingNvidiaEvidence,
585            "attestation.require_nvidia=required but nvidia_payload is absent",
586        ),
587        (NvidiaRequirement::Never, None) => Ok(NvidiaVerificationSummary::not_present()),
588        (NvidiaRequirement::Never, Some(_)) => Ok(NvidiaVerificationSummary {
589            present: true,
590            verified: NvidiaVerificationStatus::IgnoredByPolicy,
591        }),
592        (_, Some(Value::Object(_))) | (_, Some(Value::String(_))) => {
593            Err(AttestationError::ExternalVerifierUnavailable {
594                verifier: "nvidia-nras",
595                message: "NVIDIA attestation payload is present and policy requires verification, but this v0.1 verifier has no NRAS/local NVIDIA verifier backend linked".to_owned(),
596            })
597        }
598        (_, Some(_)) => policy_error(
599            AttestationFailureCode::InvalidNvidiaEvidence,
600            "nvidia_payload is present but is not an object or encoded string",
601        ),
602        (NvidiaRequirement::WhenPresent, None) => Ok(NvidiaVerificationSummary::not_present()),
603    }
604}
605
606/// Parses a TDX quote string and returns the fields needed by policy evaluation.
607fn parse_tdx_quote(value: &str) -> Result<ParsedTdxQuote, AttestationError> {
608    let bytes = decode_tdx_quote(value)?;
609
610    if bytes.len() < TDX_REPORT_DATA_END {
611        return policy_error(
612            AttestationFailureCode::InvalidTdxEvidence,
613            format!(
614                "Intel TDX quote is too short: got {} bytes, need at least {TDX_REPORT_DATA_END}",
615                bytes.len()
616            ),
617        );
618    }
619
620    let tee_type = u32::from_le_bytes(
621        bytes[TDX_QUOTE_TEE_TYPE_OFFSET..TDX_QUOTE_TEE_TYPE_END]
622            .try_into()
623            .expect("TDX tee_type slice length is fixed"),
624    );
625    let td_attributes = u64::from_le_bytes(
626        bytes[TDX_REPORT_TD_ATTRIBUTES_OFFSET..TDX_REPORT_TD_ATTRIBUTES_END]
627            .try_into()
628            .expect("TDX attributes slice length is fixed"),
629    );
630    let debug = td_attributes & 1 == 1;
631
632    Ok(ParsedTdxQuote { tee_type, debug })
633}
634
635/// Decodes a hex-encoded TDX quote.
636fn decode_tdx_quote(value: &str) -> Result<Vec<u8>, AttestationError> {
637    let value = value.trim();
638    let hex = value.strip_prefix("0x").unwrap_or(value);
639    hex::decode(hex).map_err(|source| AttestationError::PolicyViolation {
640        code: AttestationFailureCode::InvalidTdxEvidence,
641        message: format!("attestation.evidence.quote is not valid hex: {source}"),
642    })
643}
644
645/// Verifies that TDX report data binds to the attested signing key or signing address.
646fn verify_reportdata_binding(
647    reportdata_hex: &str,
648    signing_key: &str,
649    signing_address: Option<&str>,
650) -> Result<(), AttestationError> {
651    let reportdata =
652        hex::decode(reportdata_hex).map_err(|error| AttestationError::PolicyViolation {
653            code: AttestationFailureCode::InvalidTdxEvidence,
654            message: format!("quote_report_data is not valid hex: {error}"),
655        })?;
656    if reportdata.len() != TDX_REPORT_DATA_LEN {
657        return policy_error(
658            AttestationFailureCode::InvalidTdxEvidence,
659            format!(
660                "quote_report_data has {} bytes, expected {TDX_REPORT_DATA_LEN}",
661                reportdata.len()
662            ),
663        );
664    }
665
666    let signing_key_bytes =
667        hex::decode(signing_key).map_err(|error| AttestationError::PolicyViolation {
668            code: AttestationFailureCode::InvalidSigningKey,
669            message: format!("normalized signing key is not valid hex: {error}"),
670        })?;
671    let signing_key_hash = Sha256::digest(&signing_key_bytes);
672    if reportdata.starts_with(&signing_key_hash[..]) {
673        return Ok(());
674    }
675
676    if let Some(signing_address) = signing_address {
677        let signing_address_hash = Sha256::digest(signing_address.as_bytes());
678        if reportdata.starts_with(&signing_address_hash[..]) {
679            return Ok(());
680        }
681
682        let signing_address_hex = signing_address
683            .strip_prefix("0x")
684            .unwrap_or(signing_address);
685        let signing_address_bytes = hex::decode(signing_address_hex).map_err(|error| {
686            AttestationError::PolicyViolation {
687                code: AttestationFailureCode::SigningAddressMismatch,
688                message: format!("normalized signing address is not valid hex: {error}"),
689            }
690        })?;
691        if signing_address_bytes.len() == 20 && reportdata.starts_with(&signing_address_bytes) {
692            return Ok(());
693        }
694    }
695
696    policy_error(
697        AttestationFailureCode::InvalidTdxEvidence,
698        "TDX REPORTDATA does not bind the attested signing key or signing address",
699    )
700}
701
702/// Returns a non-empty string slice after trimming-only emptiness checks.
703fn non_empty(value: Option<&str>) -> Option<&str> {
704    value.filter(|value| !value.trim().is_empty())
705}
706
707/// Parses a secp256k1 public key hex string and returns uncompressed SEC1 lowercase hex.
708fn normalize_public_key_hex(value: &str) -> Result<String, AttestationError> {
709    let value = value.trim().strip_prefix("0x").unwrap_or(value.trim());
710    let mut bytes = hex::decode(value).map_err(|error| AttestationError::PolicyViolation {
711        code: AttestationFailureCode::InvalidSigningKey,
712        message: error.to_string(),
713    })?;
714
715    if bytes.len() == 64 {
716        let mut uncompressed = Vec::with_capacity(65);
717        uncompressed.push(0x04);
718        uncompressed.extend_from_slice(&bytes);
719        bytes = uncompressed;
720    }
721
722    if !matches!(bytes.len(), 33 | 65) {
723        return policy_error(
724            AttestationFailureCode::InvalidSigningKey,
725            format!(
726                "signing key must be 33-byte compressed, 64-byte x/y, or 65-byte uncompressed SEC1 public key; got {} bytes",
727                bytes.len()
728            ),
729        );
730    }
731
732    let public_key =
733        PublicKey::from_sec1_bytes(&bytes).map_err(|_| AttestationError::PolicyViolation {
734            code: AttestationFailureCode::InvalidSigningKey,
735            message: "signing key is not a valid secp256k1 public key".to_owned(),
736        })?;
737    Ok(hex::encode(public_key.to_encoded_point(false).as_bytes()))
738}
739
740/// Derives the lowercase Ethereum address for an uncompressed secp256k1 public key hex string.
741fn ethereum_address_from_uncompressed_key_hex(value: &str) -> Result<String, AttestationError> {
742    let bytes = hex::decode(value).map_err(|error| AttestationError::PolicyViolation {
743        code: AttestationFailureCode::InvalidSigningKey,
744        message: error.to_string(),
745    })?;
746    if bytes.len() != 65 || bytes.first() != Some(&0x04) {
747        return policy_error(
748            AttestationFailureCode::InvalidSigningKey,
749            "normalized signing key is not an uncompressed 65-byte SEC1 key",
750        );
751    }
752
753    let hash = Keccak256::digest(&bytes[1..]);
754    Ok(format!("0x{}", hex::encode(&hash[12..])))
755}
756
757/// Validates an Ethereum address string and returns it in lowercase `0x` form.
758fn normalize_ethereum_address(value: &str) -> Result<String, AttestationError> {
759    let value = value.trim();
760    let stripped = value.strip_prefix("0x").unwrap_or(value);
761    if stripped.len() != 40 || stripped.chars().any(|ch| !ch.is_ascii_hexdigit()) {
762        return policy_error(
763            AttestationFailureCode::SigningAddressMismatch,
764            "signing_address must be a 20-byte Ethereum address encoded as hex",
765        );
766    }
767    Ok(format!("0x{}", stripped.to_ascii_lowercase()))
768}
769
770/// Validates that a nonce is the expected number of hex characters.
771fn validate_nonce_hex(value: &str) -> Result<(), AttestationError> {
772    if value.len() != ATTESTATION_NONCE_HEX_CHARS {
773        return Err(AttestationError::InvalidRequest {
774            message: format!(
775                "attestation nonce must be {ATTESTATION_NONCE_HEX_CHARS} hex characters"
776            ),
777        });
778    }
779    if value.chars().any(|ch| !ch.is_ascii_hexdigit()) {
780        return Err(AttestationError::InvalidRequest {
781            message: "attestation nonce must contain only hex characters".to_owned(),
782        });
783    }
784    Ok(())
785}
786
787/// Returns an attestation policy-violation error with the supplied code and message.
788fn policy_error<T>(
789    code: AttestationFailureCode,
790    message: impl Into<String>,
791) -> Result<T, AttestationError> {
792    Err(AttestationError::PolicyViolation {
793        code,
794        message: message.into(),
795    })
796}
797
798#[cfg(test)]
799mod tests {
800    use super::*;
801    use std::{collections::HashMap, net::SocketAddr, time::Duration};
802
803    use axum::{
804        Router,
805        body::Body,
806        extract::Query,
807        http::{Response, StatusCode},
808        response::IntoResponse,
809        routing::get,
810    };
811    use k256::SecretKey;
812    use serde_json::json;
813    use tokio::net::TcpListener;
814
815    const MODEL: &str = "e2ee-qwen3-5-122b-a10b";
816    const NONCE: &str = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
817
818    fn policy_for_basic_success() -> AttestationConfig {
819        AttestationConfig {
820            require_tdx: false,
821            require_nvidia: NvidiaRequirement::WhenPresent,
822            ..AttestationConfig::default()
823        }
824    }
825
826    fn verifier(policy: AttestationConfig) -> AttestationVerifier {
827        AttestationVerifier::new(policy, test_venice_client("http://127.0.0.1:1/api/v1"))
828    }
829
830    fn test_venice_client(base_url: &str) -> VeniceClient {
831        VeniceClient::new(base_url, "test-api-key", Duration::from_secs(1))
832            .expect("test Venice client should build")
833    }
834
835    fn key_material() -> (String, String) {
836        let secret_key = SecretKey::from_slice(&[7_u8; 32]).expect("fixed secret key is valid");
837        let public_key = secret_key.public_key();
838        let public_key_hex = hex::encode(public_key.to_encoded_point(false).as_bytes());
839        let address = ethereum_address_from_uncompressed_key_hex(&public_key_hex)
840            .expect("test public key should derive address");
841        (public_key_hex, address)
842    }
843
844    fn reportdata_for_address(signing_address: &str) -> String {
845        let mut reportdata = vec![0_u8; TDX_REPORT_DATA_LEN];
846        let address = hex::decode(
847            signing_address
848                .strip_prefix("0x")
849                .expect("test signing address should be normalized"),
850        )
851        .expect("test signing address should be hex");
852        reportdata[..address.len()].copy_from_slice(&address);
853        hex::encode(reportdata)
854    }
855
856    fn valid_evidence() -> Value {
857        let (signing_key, signing_address) = key_material();
858        json!({
859            "api_version": "aci/1",
860            "attestation": {
861                "tee_type": "tdx",
862                "evidence": {}
863            },
864            "verified": true,
865            "nonce": NONCE,
866            "model": MODEL,
867            "tee_provider": "phala",
868            "debug": false,
869            "signing_public_key": signing_key,
870            "signing_address": signing_address
871        })
872    }
873
874    fn set_tdx_quote(evidence: &mut Value, quote: String) {
875        evidence["attestation"]["evidence"]["quote"] = json!(quote);
876    }
877
878    #[test]
879    fn generated_nonce_is_32_bytes_lower_hex() {
880        let nonce = AttestationNonce::generate();
881
882        assert_eq!(nonce.as_str().len(), 64);
883        assert!(nonce.as_str().chars().all(|ch| ch.is_ascii_hexdigit()));
884        assert!(!nonce.as_str().chars().any(|ch| ch.is_ascii_uppercase()));
885    }
886
887    #[test]
888    fn valid_basic_evidence_passes_without_optional_hardware_requirements() {
889        let result = verifier(policy_for_basic_success())
890            .verify_evidence(MODEL, NONCE, valid_evidence())
891            .expect("valid basic attestation should pass");
892
893        let (expected_key, expected_address) = key_material();
894        assert_eq!(result.model_id, MODEL);
895        assert_eq!(result.model_public_key, expected_key);
896        assert_eq!(
897            result.signing_address.as_deref(),
898            Some(expected_address.as_str())
899        );
900        assert_eq!(result.tee_provider.as_deref(), Some("phala"));
901        assert!(!result.tdx.present);
902        assert_eq!(result.nvidia.verified, NvidiaVerificationStatus::NotPresent);
903    }
904
905    #[test]
906    fn aci_envelope_uses_root_verification_fields_and_nested_hardware_evidence() {
907        let (signing_key, signing_address) = key_material();
908        let reportdata = reportdata_for_address(&signing_address);
909        let result = verifier(AttestationConfig {
910            require_tdx: false,
911            require_nvidia: NvidiaRequirement::Never,
912            ..AttestationConfig::default()
913        })
914        .verify_evidence(
915            MODEL,
916            NONCE,
917            json!({
918                "api_version": "aci/1",
919                "attestation": {
920                    "tee_type": "tdx",
921                    "evidence": {
922                        "quote": tdx_quote_hex(false, TDX_TEE_TYPE),
923                        "quote_report_data": reportdata
924                    }
925                },
926                "verified": true,
927                "nonce": NONCE,
928                "model": MODEL,
929                "tee_provider": "phala",
930                "signing_public_key": signing_key,
931                "signing_address": signing_address,
932                "nvidia_payload": {"nonce": NONCE}
933            }),
934        )
935        .expect("ACI attestation envelope should verify from root fields");
936
937        assert_eq!(result.model_id, MODEL);
938        assert_eq!(result.model_public_key, key_material().0);
939        assert_eq!(result.tee_provider.as_deref(), Some("phala"));
940        assert!(result.tdx.present);
941        assert_eq!(
942            result.nvidia.verified,
943            NvidiaVerificationStatus::IgnoredByPolicy
944        );
945    }
946
947    #[test]
948    fn missing_required_fields_fail_closed() {
949        let mut evidence = valid_evidence();
950        evidence.as_object_mut().unwrap().remove("verified");
951
952        let error = verifier(policy_for_basic_success())
953            .verify_evidence(MODEL, NONCE, evidence)
954            .expect_err("missing verified field must fail");
955
956        assert!(matches!(
957            error,
958            AttestationError::MissingField { field: "verified" }
959        ));
960        assert_eq!(error.api_error_code(), "attestation_missing_required_field");
961    }
962
963    #[test]
964    fn debug_evidence_fails_when_debug_is_not_allowed() {
965        let mut evidence = valid_evidence();
966        evidence
967            .as_object_mut()
968            .unwrap()
969            .insert("debug".to_owned(), json!(true));
970
971        let error = verifier(policy_for_basic_success())
972            .verify_evidence(MODEL, NONCE, evidence)
973            .expect_err("debug attestation must fail");
974
975        assert!(matches!(
976            error,
977            AttestationError::PolicyViolation {
978                code: AttestationFailureCode::DebugModeDetected,
979                ..
980            }
981        ));
982    }
983
984    #[test]
985    fn tdx_required_mode_fails_on_missing_tdx_evidence() {
986        let error = verifier(AttestationConfig {
987            require_tdx: true,
988            require_nvidia: NvidiaRequirement::Never,
989            ..AttestationConfig::default()
990        })
991        .verify_evidence(MODEL, NONCE, valid_evidence())
992        .expect_err("missing required TDX evidence must fail");
993
994        assert!(matches!(
995            error,
996            AttestationError::PolicyViolation {
997                code: AttestationFailureCode::MissingTdxEvidence,
998                ..
999            }
1000        ));
1001    }
1002
1003    #[test]
1004    fn tdx_required_mode_fails_on_invalid_tdx_evidence() {
1005        let mut evidence = valid_evidence();
1006        set_tdx_quote(&mut evidence, "not quote encoding".to_owned());
1007
1008        let error = verifier(AttestationConfig {
1009            require_tdx: true,
1010            require_nvidia: NvidiaRequirement::Never,
1011            ..AttestationConfig::default()
1012        })
1013        .verify_evidence(MODEL, NONCE, evidence)
1014        .expect_err("invalid TDX evidence must fail");
1015
1016        assert!(matches!(
1017            error,
1018            AttestationError::PolicyViolation {
1019                code: AttestationFailureCode::InvalidTdxEvidence,
1020                ..
1021            }
1022        ));
1023    }
1024
1025    #[test]
1026    fn tdx_debug_quote_fails_when_debug_is_not_allowed() {
1027        let mut evidence = valid_evidence();
1028        set_tdx_quote(&mut evidence, tdx_quote_hex(true, TDX_TEE_TYPE));
1029
1030        let error = verifier(AttestationConfig {
1031            require_tdx: false,
1032            require_nvidia: NvidiaRequirement::Never,
1033            allow_debug: false,
1034            ..AttestationConfig::default()
1035        })
1036        .verify_evidence(MODEL, NONCE, evidence)
1037        .expect_err("debug quote must fail");
1038
1039        assert!(matches!(
1040            error,
1041            AttestationError::PolicyViolation {
1042                code: AttestationFailureCode::DebugModeDetected,
1043                ..
1044            }
1045        ));
1046    }
1047
1048    #[test]
1049    fn tdx_required_mode_fails_closed_when_dcap_verifier_is_unavailable() {
1050        let mut evidence = valid_evidence();
1051        set_tdx_quote(&mut evidence, tdx_quote_hex(false, TDX_TEE_TYPE));
1052
1053        let error = verifier(AttestationConfig {
1054            require_tdx: true,
1055            require_nvidia: NvidiaRequirement::Never,
1056            ..AttestationConfig::default()
1057        })
1058        .verify_evidence(MODEL, NONCE, evidence)
1059        .expect_err("strict TDX should fail without DCAP verifier");
1060
1061        assert!(matches!(
1062            error,
1063            AttestationError::ExternalVerifierUnavailable {
1064                verifier: "tdx-dcap-qvl",
1065                ..
1066            }
1067        ));
1068        assert_eq!(error.api_error_code(), "attestation_verifier_unavailable");
1069    }
1070
1071    #[test]
1072    fn nvidia_required_mode_fails_on_missing_nvidia_evidence() {
1073        let error = verifier(AttestationConfig {
1074            require_tdx: false,
1075            require_nvidia: NvidiaRequirement::Required,
1076            ..AttestationConfig::default()
1077        })
1078        .verify_evidence(MODEL, NONCE, valid_evidence())
1079        .expect_err("missing required NVIDIA evidence must fail");
1080
1081        assert!(matches!(
1082            error,
1083            AttestationError::PolicyViolation {
1084                code: AttestationFailureCode::MissingNvidiaEvidence,
1085                ..
1086            }
1087        ));
1088    }
1089
1090    #[test]
1091    fn nvidia_required_mode_fails_on_invalid_nvidia_evidence() {
1092        let mut evidence = valid_evidence();
1093        evidence
1094            .as_object_mut()
1095            .unwrap()
1096            .insert("nvidia_payload".to_owned(), json!(42));
1097
1098        let error = verifier(AttestationConfig {
1099            require_tdx: false,
1100            require_nvidia: NvidiaRequirement::Required,
1101            ..AttestationConfig::default()
1102        })
1103        .verify_evidence(MODEL, NONCE, evidence)
1104        .expect_err("invalid NVIDIA evidence must fail");
1105
1106        assert!(matches!(
1107            error,
1108            AttestationError::PolicyViolation {
1109                code: AttestationFailureCode::InvalidNvidiaEvidence,
1110                ..
1111            }
1112        ));
1113    }
1114
1115    #[test]
1116    fn nvidia_payload_when_present_fails_closed_without_nras_verifier() {
1117        let mut evidence = valid_evidence();
1118        evidence
1119            .as_object_mut()
1120            .unwrap()
1121            .insert("nvidia_payload".to_owned(), json!({ "nonce": NONCE }));
1122
1123        let error = verifier(policy_for_basic_success())
1124            .verify_evidence(MODEL, NONCE, evidence)
1125            .expect_err("present NVIDIA evidence must be verified");
1126
1127        assert!(matches!(
1128            error,
1129            AttestationError::ExternalVerifierUnavailable {
1130                verifier: "nvidia-nras",
1131                ..
1132            }
1133        ));
1134    }
1135
1136    #[test]
1137    fn nonce_mismatch_fails_closed_as_stale_or_replayed_evidence() {
1138        let mut evidence = valid_evidence();
1139        evidence.as_object_mut().unwrap().insert(
1140            "nonce".to_owned(),
1141            json!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
1142        );
1143
1144        let error = verifier(policy_for_basic_success())
1145            .verify_evidence(MODEL, NONCE, evidence)
1146            .expect_err("nonce mismatch must fail");
1147
1148        assert!(matches!(
1149            error,
1150            AttestationError::PolicyViolation {
1151                code: AttestationFailureCode::NonceMismatch,
1152                ..
1153            }
1154        ));
1155    }
1156
1157    #[test]
1158    fn signing_address_mismatch_fails_closed() {
1159        let mut evidence = valid_evidence();
1160        evidence.as_object_mut().unwrap().insert(
1161            "signing_address".to_owned(),
1162            json!("0x0000000000000000000000000000000000000000"),
1163        );
1164
1165        let error = verifier(policy_for_basic_success())
1166            .verify_evidence(MODEL, NONCE, evidence)
1167            .expect_err("address mismatch must fail");
1168
1169        assert!(matches!(
1170            error,
1171            AttestationError::PolicyViolation {
1172                code: AttestationFailureCode::SigningAddressMismatch,
1173                ..
1174            }
1175        ));
1176    }
1177
1178    #[test]
1179    fn malformed_upstream_response_shape_fails_closed() {
1180        let error = verifier(policy_for_basic_success())
1181            .verify_evidence(MODEL, NONCE, json!([]))
1182            .expect_err("array response must fail");
1183
1184        assert!(matches!(error, AttestationError::MalformedResponse { .. }));
1185    }
1186
1187    #[tokio::test]
1188    async fn fetches_attestation_with_model_and_nonce_then_verifies() {
1189        let base_url = spawn_attestation_server(|query| {
1190            assert_eq!(query.get("model").map(String::as_str), Some(MODEL));
1191            let nonce = query
1192                .get("nonce")
1193                .expect("nonce query parameter should be present");
1194            assert_eq!(nonce.len(), 64);
1195            assert!(nonce.chars().all(|ch| ch.is_ascii_hexdigit()));
1196
1197            let (signing_key, signing_address) = key_material();
1198            (
1199                StatusCode::OK,
1200                serde_json::to_vec(&json!({
1201                    "api_version": "aci/1",
1202                    "attestation": {
1203                        "tee_type": "tdx",
1204                        "evidence": {}
1205                    },
1206                    "verified": true,
1207                    "nonce": nonce,
1208                    "model": MODEL,
1209                    "tee_provider": "phala",
1210                    "signing_public_key": signing_key,
1211                    "signing_address": signing_address
1212                }))
1213                .expect("response should serialize"),
1214            )
1215        })
1216        .await;
1217        let verifier =
1218            AttestationVerifier::new(policy_for_basic_success(), test_venice_client(&base_url));
1219
1220        let result = verifier
1221            .verify_model_attestation(MODEL)
1222            .await
1223            .expect("mock attestation should verify");
1224
1225        assert_eq!(result.model_id, MODEL);
1226        assert_eq!(result.model_public_key, key_material().0);
1227    }
1228
1229    #[tokio::test]
1230    async fn malformed_upstream_json_fails_closed() {
1231        let base_url = spawn_raw_attestation_server(StatusCode::OK, b"{".to_vec()).await;
1232        let verifier =
1233            AttestationVerifier::new(policy_for_basic_success(), test_venice_client(&base_url));
1234
1235        let error = verifier
1236            .verify_model_attestation(MODEL)
1237            .await
1238            .expect_err("malformed upstream JSON must fail");
1239
1240        assert!(matches!(
1241            error,
1242            AttestationError::Fetch(VeniceClientError::MalformedAttestationPayload { .. })
1243        ));
1244        assert_eq!(error.api_error_code(), "attestation_fetch_failed");
1245    }
1246
1247    #[tokio::test]
1248    async fn upstream_fetch_errors_fail_closed() {
1249        let verifier = AttestationVerifier::new(
1250            policy_for_basic_success(),
1251            test_venice_client("http://127.0.0.1:1/api/v1"),
1252        );
1253
1254        let error = verifier
1255            .verify_model_attestation(MODEL)
1256            .await
1257            .expect_err("connection failure must fail closed");
1258
1259        assert!(matches!(error, AttestationError::Fetch(_)));
1260        assert_eq!(error.api_error_code(), "attestation_fetch_failed");
1261    }
1262
1263    fn tdx_quote_hex(debug: bool, tee_type: u32) -> String {
1264        hex::encode(tdx_quote_bytes(debug, tee_type))
1265    }
1266
1267    fn tdx_quote_bytes(debug: bool, tee_type: u32) -> Vec<u8> {
1268        let mut bytes = vec![0_u8; TDX_REPORT_DATA_END];
1269        bytes[TDX_QUOTE_TEE_TYPE_OFFSET..TDX_QUOTE_TEE_TYPE_END]
1270            .copy_from_slice(&tee_type.to_le_bytes());
1271        let td_attributes = if debug { 1_u64 } else { 0_u64 };
1272        bytes[TDX_REPORT_TD_ATTRIBUTES_OFFSET..TDX_REPORT_TD_ATTRIBUTES_END]
1273            .copy_from_slice(&td_attributes.to_le_bytes());
1274        bytes
1275    }
1276
1277    async fn spawn_attestation_server<F>(handler: F) -> String
1278    where
1279        F: Fn(HashMap<String, String>) -> (StatusCode, Vec<u8>) + Clone + Send + Sync + 'static,
1280    {
1281        async fn route<F>(
1282            Query(query): Query<HashMap<String, String>>,
1283            handler: F,
1284        ) -> Response<Body>
1285        where
1286            F: Fn(HashMap<String, String>) -> (StatusCode, Vec<u8>) + Clone + Send + Sync + 'static,
1287        {
1288            let (status, body) = handler(query);
1289            (status, body).into_response()
1290        }
1291
1292        let app = Router::new().route(
1293            "/api/v1/tee/attestation",
1294            get({
1295                let handler = handler.clone();
1296                move |query| route(query, handler.clone())
1297            }),
1298        );
1299        spawn_router(app).await
1300    }
1301
1302    async fn spawn_raw_attestation_server(status: StatusCode, body: Vec<u8>) -> String {
1303        let app = Router::new().route(
1304            "/api/v1/tee/attestation",
1305            get(move || async move { (status, body.clone()) }),
1306        );
1307        spawn_router(app).await
1308    }
1309
1310    async fn spawn_router(app: Router) -> String {
1311        let listener = TcpListener::bind(("127.0.0.1", 0))
1312            .await
1313            .expect("test listener should bind");
1314        let addr: SocketAddr = listener.local_addr().expect("listener should have address");
1315        tokio::spawn(async move {
1316            axum::serve(listener, app)
1317                .await
1318                .expect("test server should run");
1319        });
1320        format!("http://{addr}/api/v1")
1321    }
1322}