pub mod binding;
pub mod dcap;
pub mod eventlog;
pub mod nvidia;
pub mod report;
pub mod signature;
pub mod tdx;
use std::sync::Arc;
use async_trait::async_trait;
use crate::cache::AttestationCache;
use crate::near::binding::{compose_matches_mr_config, report_data_binds};
use crate::near::dcap::{AciDcapVerifierPolicy, model_identity};
use crate::near::eventlog::event_log_binds_info;
use crate::near::nvidia::{NvidiaEatKey, check_nras_eat};
use crate::near::report::ModelAttestation;
use crate::near::signature::{chat_signing_text, recover_eip191_address, sha256_hex};
use crate::near::tdx::QuoteVerifier;
use crate::transport::ReportTransport;
use crate::types::{
AttestationChecks, AttestationVerdict, ExchangeInput, IntegrityProof, VerifiedExchange,
};
use crate::{ConfidentialVerifier, VerifyError};
pub const TRUST_BOUNDARY: &str = "near-ai-model";
pub const DEFAULT_CACHE_TTL_SECONDS: u64 = 600;
pub const UNVERIFIED_CACHE_TTL_SECONDS: u64 = 60;
pub struct NearVerifier {
transport: Arc<dyn ReportTransport>,
quotes: Arc<dyn QuoteVerifier>,
policy: Arc<AciDcapVerifierPolicy>,
nvidia_key: Arc<NvidiaEatKey>,
cache: AttestationCache,
cache_ttl_seconds: u64,
}
impl NearVerifier {
pub fn new(
transport: Arc<dyn ReportTransport>,
quotes: Arc<dyn QuoteVerifier>,
policy: Arc<AciDcapVerifierPolicy>,
nvidia_key: Arc<NvidiaEatKey>,
) -> Self {
Self {
transport,
quotes,
policy,
nvidia_key,
cache: AttestationCache::new(),
cache_ttl_seconds: DEFAULT_CACHE_TTL_SECONDS,
}
}
pub fn with_cache_ttl(mut self, ttl_seconds: u64) -> Self {
self.cache_ttl_seconds = ttl_seconds;
self
}
pub async fn verdict_cached(
&self,
model: &str,
now_unix: u64,
) -> Result<AttestationVerdict, VerifyError> {
if let Some(cached) = self.cache.get(model, now_unix) {
return Ok(cached);
}
let nonce = crate::fresh_nonce_hex();
let verdict = self.verify_attestation(model, &nonce, now_unix).await?;
let ttl = if verdict.verified {
self.cache_ttl_seconds
} else {
self.cache_ttl_seconds.min(UNVERIFIED_CACHE_TTL_SECONDS)
};
self.cache.put(verdict.clone(), ttl, now_unix);
Ok(verdict)
}
async fn evaluate_node(
&self,
m: &ModelAttestation,
nonce: &str,
now_unix: u64,
) -> AttestationChecks {
let Ok(raw_quote) = hex::decode(&m.intel_quote) else {
return AttestationChecks::failed();
};
let gpu_nras_pass = match self.transport.fetch_gpu_eat(&m.nvidia_payload).await {
Ok(eat) => check_nras_eat(&eat, nonce, &self.nvidia_key).passed(),
Err(_) => false,
};
let measurements = self.quotes.measurements(&raw_quote, now_unix).await.ok();
let dcap_quote_valid = measurements.is_some() && self.quotes.is_authenticated();
let nonce_echoed = m.request_nonce.eq_ignore_ascii_case(nonce);
let report_data_binds_key_and_nonce = nonce_echoed
&& measurements
.as_ref()
.is_some_and(|mm| report_data_binds(&mm.report_data, &m.signing_address, nonce));
let debug_disabled = measurements
.as_ref()
.is_some_and(super::near::tdx::TdxMeasurements::debug_disabled);
let event_log_rtmr_ok = measurements
.as_ref()
.map(|mm| event_log_binds_info(&m.event_log, &mm.rtmr3, &m.info));
let compose_matches_mr_config =
compose_matches_mr_config(&m.info.tcb_info.app_compose, &m.info.compose_hash);
let policy_accepts = match model_identity(&m.info) {
Ok(id) => {
self.policy.accepts(&id.workload_id, &id.image_digests)
&& self.policy.accepts_kms_root(&id.kms_root_public_key)
}
Err(_) => false,
};
let base_measurements_match = measurements.as_ref().is_some_and(|mm| {
self.policy
.accepts_base_measurements(&mm.mr_td, &mm.rtmr0, &mm.rtmr1, &mm.rtmr2)
});
let tcb_status = measurements.as_ref().and_then(|mm| mm.tcb_status.clone());
let tcb_level_acceptable = measurements.as_ref().is_some_and(|mm| {
self.policy
.tcb_acceptable(mm.tcb_status.as_deref(), &mm.tcb_advisory_ids)
});
AttestationChecks {
gpu_nras_pass,
dcap_quote_valid,
report_data_binds_key_and_nonce,
compose_matches_mr_config,
policy_accepts,
debug_disabled,
event_log_rtmr_ok,
base_measurements_match,
tcb_status,
tcb_level_acceptable,
}
}
}
#[async_trait]
impl ConfidentialVerifier for NearVerifier {
fn provider(&self) -> &str {
"near-ai"
}
async fn attestation_cached(
&self,
model: &str,
now_unix: u64,
) -> Result<AttestationVerdict, VerifyError> {
self.verdict_cached(model, now_unix).await
}
async fn verify_attestation(
&self,
model: &str,
nonce: &str,
now_unix: u64,
) -> Result<AttestationVerdict, VerifyError> {
let Ok(report) = self.transport.fetch_report(model, nonce).await else {
return Ok(AttestationVerdict::unverified(model, nonce, now_unix));
};
let mut attested_addresses = Vec::new();
let mut summary = AttestationChecks::failed();
let mut saw_node = false;
for m in report
.model_attestations
.iter()
.filter(|m| m.model_name == model)
{
let checks = self.evaluate_node(m, nonce, now_unix).await;
if !saw_node {
summary = checks.clone();
saw_node = true;
}
if checks.all_pass() {
summary = checks;
attested_addresses.push(m.signing_address.clone());
}
}
Ok(AttestationVerdict {
model: model.to_string(),
verified: !attested_addresses.is_empty(),
attested_addresses,
trust_boundary: TRUST_BOUNDARY.to_string(),
nonce: nonce.to_string(),
checks: summary,
verified_at_unix: now_unix,
})
}
async fn verify_exchange(
&self,
ex: &ExchangeInput<'_>,
) -> Result<VerifiedExchange, VerifyError> {
let request_hash = sha256_hex(ex.request_body);
let response_hash = sha256_hex(ex.response_body);
let expected_text = chat_signing_text(ex.model, &request_hash, &response_hash);
let attestation = self.attestation_cached(ex.model, ex.now_unix).await?;
let Ok(sig) = self.transport.fetch_signature(ex.chat_id, ex.model).await else {
return Ok(VerifiedExchange {
provider: self.provider().to_string(),
model: ex.model.to_string(),
request_hash,
response_hash,
attestation,
integrity: IntegrityProof::NearChatSignature {
text: String::new(),
signature: String::new(),
signing_address: String::new(),
},
verified: false,
});
};
let text_matches = sig.text == expected_text;
let recovered = recover_eip191_address(sig.text.as_bytes(), &sig.signature);
let recovers_to_claim = recovered
.as_deref()
.is_some_and(|r| r.eq_ignore_ascii_case(&sig.signing_address));
let attested = recovered.as_deref().is_some_and(|r| {
attestation
.attested_addresses
.iter()
.any(|a| a.eq_ignore_ascii_case(r))
});
let verified = attestation.verified && text_matches && recovers_to_claim && attested;
Ok(VerifiedExchange {
provider: self.provider().to_string(),
model: ex.model.to_string(),
request_hash,
response_hash,
attestation,
integrity: IntegrityProof::NearChatSignature {
text: sig.text,
signature: sig.signature,
signing_address: sig.signing_address,
},
verified,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::near::report::AttestationReport;
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
use std::sync::atomic::{AtomicUsize, Ordering};
const FIXTURE: &str = include_str!("../../tests/fixtures/near_report.json");
const MODEL: &str = "zai-org/GLM-5.1-FP8";
const FIXTURE_NONCE: &str = "9a01356cb451dc2c3c0ce9a195245a0be984a3f73617f55f87913fc2f059cba7";
const APP_ID: &str = "2c0a0c96cb6dbd659bf1446e2f3fce58172ff91b";
const KMS_ROOT_DER_SPKI: &str = "3059301306072a8648ce3d020106082a8648ce3d03010703420004228f800590a10442cba9d0e6adb2fa9f195eea9e75e23dd35990d52b59dda2415a63674c38adebde4ffd4d4b265bf818985933820c8053cee3ce29b5fb0fbcbc";
const TEST_EC_PRIVATE_PKCS8_PEM: &str =
include_str!("../../tests/fixtures/nras_test_ec_private_pkcs8.pem");
const TEST_EC_PUBLIC_PEM: &str = include_str!("../../tests/fixtures/nras_test_ec_public.pem");
fn nvidia_key() -> Arc<NvidiaEatKey> {
Arc::new(NvidiaEatKey::from_ec_pem(TEST_EC_PUBLIC_PEM.as_bytes()).unwrap())
}
fn signed_eat(eat_nonce: &str) -> Vec<u8> {
let ek = EncodingKey::from_ec_pem(TEST_EC_PRIVATE_PKCS8_PEM.as_bytes()).unwrap();
let claims = serde_json::json!({
"x-nvidia-overall-att-result": true,
"eat_nonce": eat_nonce,
});
let jwt = encode(&Header::new(Algorithm::ES256), &claims, &ek).unwrap();
serde_json::to_vec(&serde_json::json!([["JWT", jwt], {}])).unwrap()
}
struct StubTransport {
eat_nonce: String,
fail_report: bool,
report_fetches: AtomicUsize,
}
impl StubTransport {
fn passing() -> Self {
Self {
eat_nonce: FIXTURE_NONCE.to_string(),
fail_report: false,
report_fetches: AtomicUsize::new(0),
}
}
}
#[async_trait]
impl ReportTransport for StubTransport {
async fn fetch_report(
&self,
_model: &str,
_nonce: &str,
) -> Result<AttestationReport, VerifyError> {
self.report_fetches.fetch_add(1, Ordering::SeqCst);
if self.fail_report {
return Err(VerifyError::Malformed {
what: "report",
detail: "withheld".to_string(),
});
}
Ok(serde_json::from_str(FIXTURE).unwrap())
}
async fn fetch_gpu_eat(&self, _nvidia_payload: &str) -> Result<Vec<u8>, VerifyError> {
Ok(signed_eat(&self.eat_nonce))
}
}
struct TrustingQuoteVerifier {
tcb_status: Option<String>,
tcb_advisory_ids: Vec<String>,
}
impl TrustingQuoteVerifier {
fn up_to_date() -> Self {
Self {
tcb_status: Some("UpToDate".to_string()),
tcb_advisory_ids: Vec::new(),
}
}
fn with_tcb(status: &str, advisories: &[&str]) -> Self {
Self {
tcb_status: Some(status.to_string()),
tcb_advisory_ids: advisories.iter().map(|s| s.to_string()).collect(),
}
}
}
#[async_trait]
impl QuoteVerifier for TrustingQuoteVerifier {
async fn measurements(
&self,
raw_quote: &[u8],
_now_unix: u64,
) -> Result<crate::near::tdx::TdxMeasurements, VerifyError> {
let mut m = crate::near::tdx::parse_tdx_quote(raw_quote)?;
m.tcb_status = self.tcb_status.clone();
m.tcb_advisory_ids = self.tcb_advisory_ids.clone();
Ok(m)
}
fn is_authenticated(&self) -> bool {
true
}
}
fn policy(workload: &str, kms: &str) -> Arc<AciDcapVerifierPolicy> {
Arc::new(
AciDcapVerifierPolicy::new(
[workload.to_string()],
[],
[kms.to_string()],
[fixture_base_mrs()],
)
.unwrap(),
)
}
fn fixture_base_mrs() -> String {
let r: AttestationReport = serde_json::from_str(FIXTURE).unwrap();
let m = crate::near::tdx::parse_tdx_quote(
&hex::decode(&r.model_attestations[0].intel_quote).unwrap(),
)
.unwrap();
format!(
"{}{}{}{}",
hex::encode(m.mr_td),
hex::encode(m.rtmr0),
hex::encode(m.rtmr1),
hex::encode(m.rtmr2),
)
}
fn verifier(
transport: Arc<dyn ReportTransport>,
pol: Arc<AciDcapVerifierPolicy>,
) -> NearVerifier {
verifier_with_quotes(
transport,
pol,
Arc::new(TrustingQuoteVerifier::up_to_date()),
)
}
fn verifier_with_quotes(
transport: Arc<dyn ReportTransport>,
pol: Arc<AciDcapVerifierPolicy>,
quotes: Arc<dyn QuoteVerifier>,
) -> NearVerifier {
NearVerifier::new(transport, quotes, pol, nvidia_key())
}
#[tokio::test]
async fn verifies_the_legitimate_model_end_to_end() {
let v = verifier(
Arc::new(StubTransport::passing()),
policy(APP_ID, KMS_ROOT_DER_SPKI),
);
let verdict = v
.verify_attestation(MODEL, FIXTURE_NONCE, 1_000)
.await
.unwrap();
assert!(verdict.verified, "checks: {:?}", verdict.checks);
assert!(verdict.checks.all_pass());
assert_eq!(
verdict.attested_addresses,
vec!["0xbb4d2e7ffe98eefcd9690e2139be41e92b95e333".to_string()]
);
assert_eq!(verdict.trust_boundary, TRUST_BOUNDARY);
assert_eq!(verdict.checks.tcb_status.as_deref(), Some("UpToDate"));
assert!(verdict.checks.tcb_level_acceptable);
assert!(verdict.checks.base_measurements_match);
}
#[tokio::test]
async fn rejects_an_out_of_date_tcb_by_default() {
let quotes = Arc::new(TrustingQuoteVerifier::with_tcb(
"OutOfDate",
&["INTEL-SA-00615"],
));
let v = verifier_with_quotes(
Arc::new(StubTransport::passing()),
policy(APP_ID, KMS_ROOT_DER_SPKI),
quotes,
);
let verdict = v
.verify_attestation(MODEL, FIXTURE_NONCE, 1_000)
.await
.unwrap();
let c = &verdict.checks;
assert!(c.gpu_nras_pass && c.dcap_quote_valid && c.report_data_binds_key_and_nonce);
assert!(c.compose_matches_mr_config && c.policy_accepts && c.debug_disabled);
assert_eq!(c.event_log_rtmr_ok, Some(true));
assert!(c.base_measurements_match);
assert_eq!(c.tcb_status.as_deref(), Some("OutOfDate"));
assert!(!c.tcb_level_acceptable);
assert!(!c.all_pass());
assert!(!verdict.verified, "out-of-date TCB must not verify");
assert!(verdict.attested_addresses.is_empty());
}
#[tokio::test]
async fn accepts_an_out_of_date_tcb_when_its_advisory_is_allowlisted() {
let quotes = Arc::new(TrustingQuoteVerifier::with_tcb(
"OutOfDate",
&["INTEL-SA-00615"],
));
let pol = Arc::new(
AciDcapVerifierPolicy::new(
[APP_ID.to_string()],
[],
[KMS_ROOT_DER_SPKI.to_string()],
[fixture_base_mrs()],
)
.unwrap()
.with_allowed_tcb_advisory_ids(["INTEL-SA-00615".to_string()]),
);
let v = verifier_with_quotes(Arc::new(StubTransport::passing()), pol, quotes);
let verdict = v
.verify_attestation(MODEL, FIXTURE_NONCE, 1_000)
.await
.unwrap();
assert!(verdict.checks.tcb_level_acceptable);
assert!(verdict.checks.all_pass());
assert!(verdict.verified);
}
#[tokio::test]
async fn rejects_out_of_date_when_an_advisory_is_not_allowlisted() {
let quotes = Arc::new(TrustingQuoteVerifier::with_tcb(
"OutOfDate",
&["INTEL-SA-00615", "INTEL-SA-00999"],
));
let pol = Arc::new(
AciDcapVerifierPolicy::new(
[APP_ID.to_string()],
[],
[KMS_ROOT_DER_SPKI.to_string()],
[fixture_base_mrs()],
)
.unwrap()
.with_allowed_tcb_advisory_ids(["INTEL-SA-00615".to_string()]),
);
let v = verifier_with_quotes(Arc::new(StubTransport::passing()), pol, quotes);
let verdict = v
.verify_attestation(MODEL, FIXTURE_NONCE, 1_000)
.await
.unwrap();
assert!(!verdict.checks.tcb_level_acceptable);
assert!(!verdict.verified);
}
#[tokio::test]
async fn rejects_a_genuine_tee_when_policy_pins_a_different_model() {
let v = verifier(
Arc::new(StubTransport::passing()),
policy("some-other-workload", KMS_ROOT_DER_SPKI),
);
let verdict = v
.verify_attestation(MODEL, FIXTURE_NONCE, 1_000)
.await
.unwrap();
assert!(!verdict.verified);
assert!(!verdict.checks.policy_accepts);
assert!(verdict.attested_addresses.is_empty());
}
#[tokio::test]
async fn rejects_a_genuine_tee_whose_base_measurements_are_not_pinned() {
let mut wrong_base = fixture_base_mrs();
wrong_base.replace_range(0..2, "00"); let pol = Arc::new(
AciDcapVerifierPolicy::new(
[APP_ID.to_string()],
[],
[KMS_ROOT_DER_SPKI.to_string()],
[wrong_base],
)
.unwrap(),
);
let v = verifier(Arc::new(StubTransport::passing()), pol);
let verdict = v
.verify_attestation(MODEL, FIXTURE_NONCE, 1_000)
.await
.unwrap();
let c = &verdict.checks;
assert!(c.gpu_nras_pass && c.dcap_quote_valid && c.report_data_binds_key_and_nonce);
assert!(c.compose_matches_mr_config && c.policy_accepts && c.debug_disabled);
assert_eq!(c.event_log_rtmr_ok, Some(true));
assert!(c.tcb_level_acceptable);
assert!(
!c.base_measurements_match,
"forged base MRs must not match the pinned bundle"
);
assert!(!c.all_pass());
assert!(
!verdict.verified,
"a base image whose MRs aren't pinned must not verify (#567)"
);
assert!(verdict.attested_addresses.is_empty());
}
#[tokio::test]
async fn fail_closed_when_the_report_is_withheld() {
let transport = StubTransport {
fail_report: true,
..StubTransport::passing()
};
let v = verifier(Arc::new(transport), policy(APP_ID, KMS_ROOT_DER_SPKI));
let verdict = v
.verify_attestation(MODEL, FIXTURE_NONCE, 1_000)
.await
.unwrap();
assert!(!verdict.verified);
assert!(!verdict.checks.all_pass());
}
#[tokio::test]
async fn stale_nonce_fails_report_data_binding() {
let v = verifier(
Arc::new(StubTransport::passing()),
policy(APP_ID, KMS_ROOT_DER_SPKI),
);
let verdict = v
.verify_attestation(MODEL, "00".repeat(32).as_str(), 1_000)
.await
.unwrap();
assert!(!verdict.verified);
assert!(!verdict.checks.report_data_binds_key_and_nonce);
}
#[tokio::test]
async fn verdict_cached_fetches_once_within_ttl_then_refreshes_after() {
let transport = Arc::new(StubTransport::passing());
let v = verifier(transport.clone(), policy(APP_ID, KMS_ROOT_DER_SPKI)).with_cache_ttl(600);
v.verdict_cached(MODEL, 1_000).await.unwrap();
v.verdict_cached(MODEL, 1_030).await.unwrap();
assert_eq!(
transport.report_fetches.load(Ordering::SeqCst),
1,
"second call within the retry window hits cache"
);
v.verdict_cached(MODEL, 1_061).await.unwrap(); assert_eq!(
transport.report_fetches.load(Ordering::SeqCst),
2,
"re-verifies after the retry window"
);
}
#[tokio::test]
async fn verified_verdict_caches_for_the_full_ttl() {
let transport = Arc::new(StubTransport::passing());
let v = verifier(transport, policy(APP_ID, KMS_ROOT_DER_SPKI)).with_cache_ttl(600);
let verdict = v
.verify_attestation(MODEL, FIXTURE_NONCE, 1_000)
.await
.unwrap();
assert!(verdict.verified);
v.cache.put(verdict, 600, 1_000);
assert!(
v.cache.get(MODEL, 1_599).is_some(),
"still fresh within TTL"
);
assert!(
v.cache.get(MODEL, 1_600).is_none(),
"expired after full TTL"
);
}
use crate::near::signature::ChatSignature;
const ADDR1: &str = "7e5f4552091a69125d5dfcb7b8c2659029395bdf";
fn sign_chat(priv_lsb: u8, text: &str) -> String {
use k256::ecdsa::SigningKey;
use sha3::{Digest, Keccak256};
let mut pk = [0u8; 32];
pk[31] = priv_lsb;
let key = SigningKey::from_slice(&pk).unwrap();
let mut h = Keccak256::new();
h.update(b"\x19Ethereum Signed Message:\n");
h.update(text.len().to_string().as_bytes());
h.update(text.as_bytes());
let digest: [u8; 32] = h.finalize().into();
let (sig, rec) = key.sign_prehash_recoverable(&digest).unwrap();
let mut out = sig.to_bytes().to_vec();
out.push(27 + rec.to_byte());
hex::encode(out)
}
fn spliced_report(nonce_hex: &str) -> AttestationReport {
let mut report: AttestationReport = serde_json::from_str(FIXTURE).unwrap();
let m = &mut report.model_attestations[0];
let mut quote = hex::decode(&m.intel_quote).unwrap();
let addr = hex::decode(ADDR1).unwrap();
let nonce = hex::decode(nonce_hex).unwrap();
let mut rd = [0u8; 64];
rd[..20].copy_from_slice(&addr);
rd[32..64].copy_from_slice(&nonce);
quote[568..632].copy_from_slice(&rd);
m.intel_quote = hex::encode("e);
m.signing_address = format!("0x{ADDR1}");
m.request_nonce = nonce_hex.to_string();
report
}
struct ExchangeStub {
last_nonce: std::sync::Mutex<String>,
chat_sig: Option<ChatSignature>,
}
#[async_trait]
impl ReportTransport for ExchangeStub {
async fn fetch_report(
&self,
_model: &str,
nonce: &str,
) -> Result<AttestationReport, VerifyError> {
*self.last_nonce.lock().unwrap() = nonce.to_string();
Ok(spliced_report(nonce))
}
async fn fetch_gpu_eat(&self, _payload: &str) -> Result<Vec<u8>, VerifyError> {
Ok(signed_eat(&self.last_nonce.lock().unwrap()))
}
async fn fetch_signature(
&self,
_chat_id: &str,
_model: &str,
) -> Result<ChatSignature, VerifyError> {
self.chat_sig.clone().ok_or(VerifyError::Malformed {
what: "chat signature",
detail: "withheld".to_string(),
})
}
}
fn exchange_verifier(chat_sig: Option<ChatSignature>) -> NearVerifier {
NearVerifier::new(
Arc::new(ExchangeStub {
last_nonce: std::sync::Mutex::new(String::new()),
chat_sig,
}),
Arc::new(TrustingQuoteVerifier::up_to_date()),
policy(APP_ID, KMS_ROOT_DER_SPKI),
nvidia_key(),
)
}
fn signature_over(model: &str, req: &[u8], resp: &[u8], priv_lsb: u8) -> ChatSignature {
let text =
crate::chat_signing_text(model, &crate::sha256_hex(req), &crate::sha256_hex(resp));
let signature = sign_chat(priv_lsb, &text);
let signing_address = crate::recover_eip191_address(text.as_bytes(), &signature).unwrap();
ChatSignature {
text,
signature,
signing_address,
signing_algo: "ecdsa".to_string(),
}
}
#[tokio::test]
async fn verifies_a_genuine_exchange_end_to_end() {
let (req, resp) = (
b"the request bytes".as_slice(),
b"the response bytes".as_slice(),
);
let sig = signature_over(MODEL, req, resp, 1);
let v = exchange_verifier(Some(sig));
let ex = crate::ExchangeInput {
model: MODEL,
request_body: req,
response_body: resp,
chat_id: "chat-1",
now_unix: 1_000,
};
let out = v.verify_exchange(&ex).await.unwrap();
assert!(out.verified, "attestation: {:?}", out.attestation.checks);
assert_eq!(out.request_hash, crate::sha256_hex(req));
assert!(matches!(
out.integrity,
crate::IntegrityProof::NearChatSignature { .. }
));
}
#[tokio::test]
async fn tampered_response_body_fails_the_text_match() {
let (req, resp) = (b"req".as_slice(), b"original response".as_slice());
let sig = signature_over(MODEL, req, resp, 1);
let v = exchange_verifier(Some(sig));
let ex = crate::ExchangeInput {
model: MODEL,
request_body: req,
response_body: b"tampered response",
chat_id: "chat-1",
now_unix: 1_000,
};
let out = v.verify_exchange(&ex).await.unwrap();
assert!(!out.verified);
}
#[tokio::test]
async fn signature_from_an_unattested_key_fails() {
let (req, resp) = (b"req".as_slice(), b"resp".as_slice());
let sig = signature_over(MODEL, req, resp, 2);
let v = exchange_verifier(Some(sig));
let ex = crate::ExchangeInput {
model: MODEL,
request_body: req,
response_body: resp,
chat_id: "chat-1",
now_unix: 1_000,
};
let out = v.verify_exchange(&ex).await.unwrap();
assert!(!out.verified);
}
#[tokio::test]
async fn withheld_signature_is_fail_closed() {
let (req, resp) = (b"req".as_slice(), b"resp".as_slice());
let v = exchange_verifier(None); let ex = crate::ExchangeInput {
model: MODEL,
request_body: req,
response_body: resp,
chat_id: "chat-1",
now_unix: 1_000,
};
let out = v.verify_exchange(&ex).await.unwrap();
assert!(!out.verified);
}
}