1use 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#[derive(Clone, Debug)]
48pub struct AttestationVerifier {
49 policy: AttestationConfig,
50 venice_client: VeniceClient,
51}
52
53#[derive(Clone, PartialEq, Eq)]
55pub struct AttestationNonce(String);
56
57#[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#[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#[derive(Debug, Clone, PartialEq, Eq)]
81pub struct NvidiaVerificationSummary {
82 pub present: bool,
83 pub verified: NvidiaVerificationStatus,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum NvidiaVerificationStatus {
89 NotPresent,
90 IgnoredByPolicy,
91 PresentVerifierUnavailable,
92}
93
94#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134struct ParsedTdxQuote {
135 tee_type: u32,
136 debug: bool,
137}
138
139#[derive(Debug, Clone, Deserialize)]
141struct VeniceAttestationResponse {
142 attestation: AciAttestationEnvelope,
143 #[serde(flatten)]
144 fields: VeniceAttestationFields,
145}
146
147#[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#[derive(Debug, Clone, Deserialize)]
170struct AciAttestationEnvelope {
171 #[serde(default)]
172 evidence: AciEvidenceFields,
173}
174
175#[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 pub fn from_config(config: &ProxyConfig, venice_client: VeniceClient) -> Self {
187 Self::new(config.attestation.clone(), venice_client)
188 }
189
190 pub fn new(policy: AttestationConfig, venice_client: VeniceClient) -> Self {
192 Self {
193 policy,
194 venice_client,
195 }
196 }
197
198 pub fn policy(&self) -> &AttestationConfig {
200 &self.policy
201 }
202
203 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 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 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 pub fn as_str(&self) -> &str {
251 &self.0
252 }
253}
254
255impl fmt::Debug for AttestationNonce {
256 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 f.debug_tuple("AttestationNonce").field(&self.0).finish()
259 }
260}
261
262impl TdxVerificationSummary {
263 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 fn not_present() -> Self {
277 Self {
278 present: false,
279 verified: NvidiaVerificationStatus::NotPresent,
280 }
281 }
282}
283
284impl NvidiaVerificationStatus {
285 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 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 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 pub fn verifier_unavailable(&self) -> bool {
322 matches!(self, Self::ExternalVerifierUnavailable { .. })
323 }
324}
325
326impl AttestationFailureCode {
327 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 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 fn fields(&self) -> &VeniceAttestationFields {
354 &self.fields
355 }
356
357 fn tdx_quote(&self) -> Option<&str> {
359 non_empty(self.attestation.evidence.quote.as_deref())
360 }
361
362 fn quote_report_data(&self) -> Option<&str> {
364 non_empty(self.attestation.evidence.quote_report_data.as_deref())
365 }
366}
367
368impl VeniceAttestationFields {
369 fn required_verified(&self) -> Result<bool, AttestationError> {
371 self.verified
372 .ok_or(AttestationError::MissingField { field: "verified" })
373 }
374
375 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
421fn 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
513fn 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
575fn 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
606fn 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
635fn 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
645fn 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
702fn non_empty(value: Option<&str>) -> Option<&str> {
704 value.filter(|value| !value.trim().is_empty())
705}
706
707fn 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
740fn 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
757fn 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
770fn 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
787fn 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}