use std::net::SocketAddr;
use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
use std::sync::{Arc, Mutex};
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use serde::Serialize;
use tokio::net::TcpListener;
use trust_tasks_https::{BearerAuth, ClientError, HttpsClient, HttpsServer};
use trust_tasks_rs::{
specs::acl::{grant, list, revoke, show},
specs::trust_task_discovery::v0_1 as discovery,
DocumentDigest, FreshnessPolicy, InMemoryReplayGuard, Proof, ProofVerifier, RejectReason,
ReplayGuard, ReplayGuardError, ReplayVerdict, StandardCode, TrustTask, TypeUri,
VerificationError,
};
const SERVER_VID: &str = "did:web:maintainer.example";
struct AcceptAllVerifier;
#[async_trait::async_trait]
impl ProofVerifier for AcceptAllVerifier {
async fn verify<P>(&self, _doc: &TrustTask<P>) -> Result<(), VerificationError>
where
P: Serialize + Send + Sync,
{
Ok(())
}
}
struct RejectAllVerifier;
#[async_trait::async_trait]
impl ProofVerifier for RejectAllVerifier {
async fn verify<P>(&self, _doc: &TrustTask<P>) -> Result<(), VerificationError>
where
P: Serialize + Send + Sync,
{
Err(VerificationError::SignatureInvalid)
}
}
enum VerifierMode {
None,
AcceptAll,
RejectAll,
}
async fn spawn_server_with(verifier: VerifierMode) -> SocketAddr {
let auth = BearerAuth::from_pairs([
("alice", "did:web:alice.example"),
("eve", "did:web:eve.example"),
]);
let mut builder = HttpsServer::builder()
.local_vid(SERVER_VID)
.with_auth(auth)
.on::<grant::v0_1::Payload, grant::v0_1::Response, _>(|req, _ctx| {
Ok(grant::v0_1::Response {
entry: req.payload.entry.clone(),
ext: None,
})
})
.on::<revoke::v0_1::Payload, revoke::v0_1::Response, _>(|_req, _ctx| {
Ok(revoke::v0_1::Response {
entry: None,
ext: None,
})
})
.on::<list::v0_1::Payload, list::v0_1::Response, _>(|_req, ctx| {
if ctx.authenticated_sender.as_deref() != Some("did:web:alice.example") {
return Err(RejectReason::PermissionDenied {
reason: "list is restricted".into(),
});
}
assert_eq!(
ctx.resolved.issuer.as_deref(),
Some("did:web:alice.example")
);
assert_eq!(ctx.resolved.recipient.as_deref(), Some(SERVER_VID));
Ok(list::v0_1::Response {
entries: vec![],
cursor: None,
redacted_fields: vec![],
truncated: false,
ext: None,
})
})
.enable_discovery();
builder = match verifier {
VerifierMode::None => builder,
VerifierMode::AcceptAll => builder.with_verifier(AcceptAllVerifier),
VerifierMode::RejectAll => builder.with_verifier(RejectAllVerifier),
};
let server = builder.build();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = server.into_router();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
addr
}
async fn spawn_server() -> SocketAddr {
spawn_server_with(VerifierMode::AcceptAll).await
}
fn entry() -> grant::v0_1::AclEntry {
grant::v0_1::AclEntry {
subject: "did:web:carol.example".into(),
role: "admin".parse().unwrap(),
scopes: vec![],
allowed_keys: None,
label: None,
created_at: None,
created_by: None,
updated_at: None,
updated_by: None,
expires_at: None,
approve: None,
step_up: None,
ext: None,
}
}
fn build_client(addr: SocketAddr, my_vid: &str, my_token: Option<&str>) -> HttpsClient {
let mut builder = HttpsClient::builder()
.server_url(format!("http://{addr}"))
.server_vid(SERVER_VID)
.my_vid(my_vid);
if let Some(t) = my_token {
builder = builder.my_token(t);
}
builder.build().unwrap()
}
#[tokio::test]
async fn happy_path_acl_list() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-list-1",
list::v0_1::Payload {
role: None,
scope: None,
direction: None,
subject_prefix: None,
page_size: None,
cursor: None,
ext: None,
},
);
let resp = client
.send::<list::v0_1::Payload, list::v0_1::Response>(req)
.await
.unwrap();
assert_eq!(
resp.type_uri,
"https://trusttasks.org/spec/acl/list/0.1#response"
.parse::<TypeUri>()
.unwrap()
);
assert!(resp.payload.entries.is_empty());
assert!(!resp.payload.truncated);
assert_eq!(resp.thread_id.as_deref(), Some("urn:uuid:test-list-1"));
assert_eq!(resp.recipient.as_deref(), Some("did:web:alice.example"));
}
#[tokio::test]
async fn identity_mismatch_when_in_band_issuer_differs_from_token() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:carol.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-mismatch",
grant::v0_1::Payload {
entry: entry(),
reason: None,
ext: None,
},
);
let err = client
.send::<grant::v0_1::Payload, grant::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 422);
assert_eq!(error.payload.code, StandardCode::IdentityMismatch.into());
let msg = error.payload.message.as_deref().unwrap_or("");
assert!(!msg.contains("alice"), "wire leak: {msg}");
assert!(!msg.contains("carol"), "wire leak: {msg}");
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
}
#[tokio::test]
async fn unsupported_type_for_unregistered_uri() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-unsupported",
show::v0_1::Payload {
subject: "did:web:bob.example".parse().unwrap(),
ext: None,
},
);
let err = client
.send::<show::v0_1::Payload, show::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 422);
assert_eq!(error.payload.code, StandardCode::UnsupportedType.into());
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
}
#[tokio::test]
async fn discovery_advertises_registered_handlers() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-discover-all",
discovery::Payload { patterns: vec![] },
);
let resp = client
.send::<discovery::Payload, discovery::Response>(req)
.await
.unwrap();
let mut got: Vec<&str> = resp.payload.supported_types.iter().map(uri_of).collect();
got.sort();
assert_eq!(
got,
vec![
"https://trusttasks.org/spec/acl/grant/0.1",
"https://trusttasks.org/spec/acl/list/0.1",
"https://trusttasks.org/spec/acl/revoke/0.1",
"https://trusttasks.org/spec/trust-task-discovery/0.1",
],
"enable_discovery() should advertise the registered acl/* handlers plus discovery itself"
);
assert_eq!(
resp.type_uri,
"https://trusttasks.org/spec/trust-task-discovery/0.1#response"
.parse::<TypeUri>()
.unwrap()
);
assert_eq!(
resp.thread_id.as_deref(),
Some("urn:uuid:test-discover-all")
);
}
#[tokio::test]
async fn discovery_filter_returns_only_matching_slugs() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-discover-acl",
discovery::Payload {
patterns: vec!["acl/*".parse().unwrap()],
},
);
let resp = client
.send::<discovery::Payload, discovery::Response>(req)
.await
.unwrap();
let mut got: Vec<&str> = resp.payload.supported_types.iter().map(uri_of).collect();
got.sort();
assert_eq!(
got,
vec![
"https://trusttasks.org/spec/acl/grant/0.1",
"https://trusttasks.org/spec/acl/list/0.1",
"https://trusttasks.org/spec/acl/revoke/0.1",
],
"acl/* should match the three acl handlers but not trust-task-discovery"
);
}
fn uri_of(entry: &discovery::ResponseSupportedTypesItem) -> &str {
match entry {
discovery::ResponseSupportedTypesItem::Uri(s) => s.as_str(),
discovery::ResponseSupportedTypesItem::Object { type_, .. } => type_.as_str(),
}
}
#[tokio::test]
async fn proof_required_when_spec_requires_and_doc_lacks_proof() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let req = TrustTask::for_payload(
"urn:uuid:test-proof-required",
grant::v0_1::Payload {
entry: entry(),
reason: None,
ext: None,
},
);
let err = client
.send::<grant::v0_1::Payload, grant::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 422);
assert_eq!(error.payload.code, StandardCode::ProofRequired.into());
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
}
#[tokio::test]
async fn proof_bearing_with_identity_mismatch_routes_to_transport_peer() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:carol.example", Some("alice"));
let mut req = TrustTask::for_payload(
"urn:uuid:test-proof-and-mismatch",
grant::v0_1::Payload {
entry: entry(),
reason: None,
ext: None,
},
);
req.proof = Some(Proof {
proof_type: "DataIntegrityProof".into(),
cryptosuite: "eddsa-rdfc-2022".into(),
verification_method: "did:web:carol.example#key-1".into(),
created: chrono::Utc::now(),
proof_purpose: "assertionMethod".into(),
proof_value: "z3kg".into(),
extra: Default::default(),
});
let err = client
.send::<grant::v0_1::Payload, grant::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 422);
assert_eq!(error.payload.code, StandardCode::IdentityMismatch.into());
let msg = error.payload.message.as_deref().unwrap_or("");
assert!(!msg.contains("alice"), "wire leak: {msg}");
assert!(!msg.contains("carol"), "wire leak: {msg}");
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
}
#[tokio::test]
async fn proof_bearing_document_rejected_when_server_has_no_verifier() {
let addr = spawn_server_with(VerifierMode::None).await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let mut req = TrustTask::for_payload(
"urn:uuid:test-proof-rejected",
grant::v0_1::Payload {
entry: entry(),
reason: None,
ext: None,
},
);
req.proof = Some(Proof {
proof_type: "DataIntegrityProof".into(),
cryptosuite: "eddsa-rdfc-2022".into(),
verification_method: "did:web:alice.example#key-1".into(),
created: chrono::Utc::now(),
proof_purpose: "assertionMethod".into(),
proof_value: "z3kg".into(),
extra: Default::default(),
});
let err = client
.send::<grant::v0_1::Payload, grant::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 400);
assert_eq!(error.payload.code, StandardCode::MalformedRequest.into());
let msg = error.payload.message.as_deref().unwrap_or("");
assert!(
msg.contains("policy") && msg.contains("§7.2"),
"message should cite the spec rule, not internals: {msg}"
);
assert!(!msg.contains("verifier"), "wire leak (config): {msg}");
assert!(!msg.contains("configured"), "wire leak (config): {msg}");
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
}
#[tokio::test]
async fn happy_path_acl_grant_with_verifier() {
let addr = spawn_server().await; let client = build_client(addr, "did:web:alice.example", Some("alice"));
let mut req = TrustTask::for_payload(
"urn:uuid:test-grant-verified",
grant::v0_1::Payload {
entry: entry(),
reason: None,
ext: None,
},
);
req.proof = Some(Proof {
proof_type: "DataIntegrityProof".into(),
cryptosuite: "eddsa-rdfc-2022".into(),
verification_method: "did:web:alice.example#key-1".into(),
created: chrono::Utc::now(),
proof_purpose: "assertionMethod".into(),
proof_value: "z3kg".into(),
extra: Default::default(),
});
let resp = client
.send::<grant::v0_1::Payload, grant::v0_1::Response>(req)
.await
.unwrap();
assert_eq!(
resp.type_uri,
"https://trusttasks.org/spec/acl/grant/0.1#response"
.parse::<TypeUri>()
.unwrap()
);
assert_eq!(&*resp.payload.entry.role, "admin");
assert_eq!(resp.recipient.as_deref(), Some("did:web:alice.example"));
}
#[tokio::test]
async fn proof_invalid_wire_message_withholds_the_verifier_description() {
let addr = spawn_server_with(VerifierMode::RejectAll).await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let mut req = TrustTask::for_payload(
"urn:uuid:test-proof-invalid",
list::v0_1::Payload {
role: None,
scope: None,
direction: None,
subject_prefix: None,
page_size: None,
cursor: None,
ext: None,
},
);
req.proof = Some(Proof {
proof_type: "DataIntegrityProof".into(),
cryptosuite: "eddsa-rdfc-2022".into(),
verification_method: "did:web:alice.example#key-1".into(),
created: chrono::Utc::now(),
proof_purpose: "assertionMethod".into(),
proof_value: "z3kg".into(),
extra: Default::default(),
});
let err = client
.send::<list::v0_1::Payload, list::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 422);
assert_eq!(error.payload.code, StandardCode::ProofInvalid.into());
let msg = error.payload.message.as_deref().unwrap_or("");
assert_eq!(msg, trust_tasks_rs::PROOF_INVALID_WIRE_MESSAGE);
for leaked in ["signature", "resolve", "did:web:", "verificationMethod"] {
assert!(!msg.contains(leaked), "verifier detail on the wire: {msg}");
}
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
let reason = RejectReason::ProofInvalid {
reason: VerificationError::SignatureInvalid.to_string(),
};
assert!(
reason.to_string().contains("signature"),
"the operator-facing rendering lost the verifier's description: {reason}"
);
assert_ne!(reason.to_string(), reason.wire_message());
}
#[tokio::test]
async fn permission_denied_from_spec_handler() {
let addr = spawn_server().await;
let client = build_client(addr, "did:web:eve.example", Some("eve"));
let req = TrustTask::for_payload(
"urn:uuid:test-list-unauthorized",
list::v0_1::Payload {
role: None,
scope: None,
direction: None,
subject_prefix: None,
page_size: None,
cursor: None,
ext: None,
},
);
let err = client
.send::<list::v0_1::Payload, list::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 403);
assert_eq!(error.payload.code, StandardCode::PermissionDenied.into());
}
other => panic!("expected TrustTaskError, got {other:?}"),
}
}
#[tokio::test]
async fn oversized_body_is_rejected_before_processing() {
let addr = spawn_server().await;
let big = vec![b'a'; 512 * 1024];
let resp = reqwest::Client::new()
.post(format!("http://{addr}/trust-tasks"))
.body(big)
.send()
.await
.unwrap();
assert_eq!(resp.status(), reqwest::StatusCode::PAYLOAD_TOO_LARGE);
}
#[tokio::test]
async fn deeply_nested_body_fails_to_parse_not_overflow() {
let addr = spawn_server().await;
let body = "[".repeat(1000) + &"]".repeat(1000);
let resp = reqwest::Client::new()
.post(format!("http://{addr}/trust-tasks"))
.header("content-type", "application/json")
.body(body)
.send()
.await
.unwrap();
assert_eq!(resp.status(), reqwest::StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn suppressed_identity_mismatch_is_indistinguishable_from_parse_failure() {
let addr = spawn_server().await;
let client = reqwest::Client::new();
let url = format!("http://{addr}/trust-tasks");
let mismatch = client
.post(&url)
.header("content-type", "application/json")
.body(
serde_json::json!({
"id": "urn:uuid:probe",
"type": "https://trusttasks.org/spec/acl/grant/0.1",
"issuer": "did:web:alice.example",
"recipient": "did:web:wrong.example",
"payload": { "entry": { "subject": "did:web:carol.example", "role": "admin" } }
})
.to_string(),
)
.send()
.await
.unwrap();
let mismatch_status = mismatch.status();
let mismatch_code =
mismatch.json::<serde_json::Value>().await.unwrap()["payload"]["code"].clone();
let garbage = client
.post(&url)
.header("content-type", "application/json")
.body("not json")
.send()
.await
.unwrap();
let garbage_status = garbage.status();
let garbage_code =
garbage.json::<serde_json::Value>().await.unwrap()["payload"]["code"].clone();
assert_eq!(mismatch_status, reqwest::StatusCode::BAD_REQUEST);
assert_eq!(
mismatch_status, garbage_status,
"status must not distinguish the two"
);
assert_eq!(mismatch_code, serde_json::json!("malformedRequest"));
assert_eq!(
mismatch_code, garbage_code,
"body code must not distinguish the two"
);
}
#[tokio::test]
async fn https_enforces_recipient_required_with_no_in_band_recipient() {
let addr = spawn_server().await;
let resp = reqwest::Client::new()
.post(format!("http://{addr}/trust-tasks"))
.header("authorization", "Bearer alice")
.header("content-type", "application/json")
.body(
serde_json::json!({
"id": "urn:uuid:no-recip",
"type": "https://trusttasks.org/spec/acl/grant/0.1",
"issuer": "did:web:alice.example",
"issuedAt": chrono::Utc::now().to_rfc3339(),
"payload": { "entry": { "subject": "did:web:carol.example", "role": "admin" } }
})
.to_string(),
)
.send()
.await
.unwrap();
let status = resp.status();
let code = resp.json::<serde_json::Value>().await.unwrap()["payload"]["code"].clone();
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
assert_eq!(code, serde_json::json!("malformedRequest"));
}
#[tokio::test]
async fn client_times_out_on_a_silent_server() {
use std::time::Duration;
use trust_tasks_https::HttpsClient;
use trust_tasks_rs::specs::acl::grant::v0_1 as grant;
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let mut held = Vec::new();
loop {
let Ok((socket, _)) = listener.accept().await else {
break;
};
held.push(socket);
}
});
let client = HttpsClient::builder()
.server_url(format!("http://{addr}"))
.server_vid("did:web:server.example")
.my_vid("did:web:alice.example")
.timeout(Duration::from_millis(200))
.build()
.unwrap();
let request = trust_tasks_rs::TrustTask::for_payload(
"urn:uuid:timeout-test".to_string(),
grant::Payload {
entry: grant::AclEntry {
subject: "did:web:carol.example".into(),
role: "moderator".into(),
scopes: vec![],
allowed_keys: None,
label: None,
created_at: None,
created_by: None,
updated_at: None,
updated_by: None,
expires_at: None,
approve: None,
step_up: None,
ext: None,
},
reason: None,
ext: None,
},
);
let started = std::time::Instant::now();
let err = client
.send::<grant::Payload, grant::Response>(request)
.await
.unwrap_err();
assert!(
started.elapsed() < Duration::from_secs(5),
"the call must fail fast, not hang"
);
match err {
trust_tasks_https::ClientError::Http(e) => assert!(e.is_timeout(), "got: {e}"),
other => panic!("expected Http timeout error, got: {other}"),
}
}
async fn spawn_spy_server(require_attribution: bool) -> (SocketAddr, Arc<Mutex<Vec<String>>>) {
let seen: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
let recorder = Arc::clone(&seen);
let server = HttpsServer::builder()
.local_vid(SERVER_VID)
.with_auth(BearerAuth::from_pairs([("alice", "did:web:alice.example")]))
.require_attribution(require_attribution)
.on::<list::v0_1::Payload, list::v0_1::Response, _>(move |_req, ctx| {
recorder
.lock()
.unwrap()
.push(ctx.resolved.issuer.clone().unwrap_or_default());
Ok(list::v0_1::Response {
entries: vec![],
cursor: None,
redacted_fields: vec![],
truncated: false,
ext: None,
})
})
.build();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = server.into_router();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
(addr, seen)
}
fn list_payload() -> list::v0_1::Payload {
list::v0_1::Payload {
role: None,
scope: None,
direction: None,
subject_prefix: None,
page_size: None,
cursor: None,
ext: None,
}
}
#[tokio::test]
async fn unattributable_document_is_rejected_before_the_handler() {
let (addr, seen) = spawn_spy_server(true).await;
let client = build_client(addr, "did:web:victim.example", None);
let err = client
.send::<list::v0_1::Payload, list::v0_1::Response>(TrustTask::for_payload(
"urn:uuid:test-unattributable",
list_payload(),
))
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 422);
assert_eq!(error.payload.code, StandardCode::ProofRequired.into());
}
other => panic!("expected proofRequired, got {other:?}"),
}
assert!(
seen.lock().unwrap().is_empty(),
"the handler MUST NOT run for an unattributable document; it saw {:?}",
seen.lock().unwrap()
);
}
#[tokio::test]
async fn proof_bearing_document_passes_the_attribution_gate() {
let (addr, _seen) = spawn_spy_server(true).await;
let client = build_client(addr, "did:web:alice.example", None);
let mut req = TrustTask::for_payload("urn:uuid:test-attributed-by-proof", list_payload());
req.proof = Some(Proof {
proof_type: "DataIntegrityProof".into(),
cryptosuite: "eddsa-rdfc-2022".into(),
verification_method: "did:web:alice.example#key-1".into(),
created: chrono::Utc::now(),
proof_purpose: "assertionMethod".into(),
proof_value: "z3kg".into(),
extra: Default::default(),
});
let err = client
.send::<list::v0_1::Payload, list::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { error, .. } => assert_eq!(
error.payload.code,
StandardCode::MalformedRequest.into(),
"must fail on the no-verifier policy, not on attribution"
),
other => panic!("expected the no-verifier rejection, got {other:?}"),
}
}
#[tokio::test]
async fn require_attribution_false_restores_the_permissive_path() {
let (addr, seen) = spawn_spy_server(false).await;
let client = build_client(addr, "did:web:victim.example", None);
client
.send::<list::v0_1::Payload, list::v0_1::Response>(TrustTask::for_payload(
"urn:uuid:test-optout",
list_payload(),
))
.await
.unwrap();
assert_eq!(
seen.lock().unwrap().as_slice(),
["did:web:victim.example".to_string()]
);
}
struct SpyVerifier(Arc<AtomicUsize>);
#[async_trait::async_trait]
impl ProofVerifier for SpyVerifier {
async fn verify<P>(&self, _doc: &TrustTask<P>) -> Result<(), VerificationError>
where
P: Serialize + Send + Sync,
{
self.0.fetch_add(1, AtomicOrdering::SeqCst);
Ok(())
}
}
#[tokio::test]
async fn unknown_type_is_rejected_before_the_verifier_is_called() {
let calls = Arc::new(AtomicUsize::new(0));
let server = HttpsServer::builder()
.local_vid(SERVER_VID)
.with_auth(BearerAuth::from_pairs([("alice", "did:web:alice.example")]))
.with_verifier(SpyVerifier(Arc::clone(&calls)))
.on::<list::v0_1::Payload, list::v0_1::Response, _>(|_req, _ctx| {
Ok(list::v0_1::Response {
entries: vec![],
cursor: None,
redacted_fields: vec![],
truncated: false,
ext: None,
})
})
.build();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = server.into_router();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let mut req = TrustTask::for_payload(
"urn:uuid:test-ssrf-ordering",
show::v0_1::Payload {
subject: "did:web:bob.example".parse().unwrap(),
ext: None,
},
);
req.proof = Some(Proof {
proof_type: "DataIntegrityProof".into(),
cryptosuite: "eddsa-rdfc-2022".into(),
verification_method: "did:web:attacker-chosen.example#key-1".into(),
created: chrono::Utc::now(),
proof_purpose: "assertionMethod".into(),
proof_value: "z3kg".into(),
extra: Default::default(),
});
let err = client
.send::<show::v0_1::Payload, show::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { error, .. } => {
assert_eq!(error.payload.code, StandardCode::UnsupportedType.into())
}
other => panic!("expected unsupportedType, got {other:?}"),
}
assert_eq!(
calls.load(AtomicOrdering::SeqCst),
0,
"the verifier MUST NOT be reachable via a type this server does not route"
);
}
#[tokio::test]
async fn disallowed_did_method_never_reaches_the_verifier() {
let calls = Arc::new(AtomicUsize::new(0));
let server = HttpsServer::builder()
.local_vid(SERVER_VID)
.with_auth(BearerAuth::from_pairs([("alice", "did:web:alice.example")]))
.with_verifier(SpyVerifier(Arc::clone(&calls)))
.allowed_did_methods(["key"])
.on::<list::v0_1::Payload, list::v0_1::Response, _>(|_req, _ctx| {
Ok(list::v0_1::Response {
entries: vec![],
cursor: None,
redacted_fields: vec![],
truncated: false,
ext: None,
})
})
.build();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = server.into_router();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let mut req = TrustTask::for_payload("urn:uuid:test-did-method-screen", list_payload());
req.proof = Some(Proof {
proof_type: "DataIntegrityProof".into(),
cryptosuite: "eddsa-rdfc-2022".into(),
verification_method: "did:web:attacker-chosen.example#key-1".into(),
created: chrono::Utc::now(),
proof_purpose: "assertionMethod".into(),
proof_value: "z3kg".into(),
extra: Default::default(),
});
let err = client
.send::<list::v0_1::Payload, list::v0_1::Response>(req)
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { error, .. } => {
assert_eq!(error.payload.code, StandardCode::ProofInvalid.into());
let msg = error.payload.message.as_deref().unwrap_or("");
assert!(!msg.contains("key"), "wire leak (policy): {msg}");
}
other => panic!("expected proofInvalid, got {other:?}"),
}
assert_eq!(calls.load(AtomicOrdering::SeqCst), 0);
}
async fn spawn_canned_server(status: u16, body: serde_json::Value) -> SocketAddr {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = axum::Router::new().route(
"/trust-tasks",
axum::routing::post(move || {
let body = body.clone();
async move {
(
axum::http::StatusCode::from_u16(status).unwrap(),
[(axum::http::header::CONTENT_TYPE, "application/json")],
serde_json::to_vec(&body).unwrap(),
)
}
}),
);
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
addr
}
fn well_formed_list_response(request_id: &str) -> serde_json::Value {
let mut req = TrustTask::for_payload(request_id.to_string(), list_payload());
req.issuer = Some("did:web:alice.example".into());
req.recipient = Some(SERVER_VID.into());
let resp = req.respond_with(
"urn:uuid:canned-response",
list::v0_1::Response {
entries: vec![],
cursor: None,
redacted_fields: vec![],
truncated: false,
ext: None,
},
);
serde_json::to_value(&resp).unwrap()
}
async fn send_against_canned(body: serde_json::Value, request_id: &str) -> ClientError {
let addr = spawn_canned_server(200, body).await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
client
.send::<list::v0_1::Payload, list::v0_1::Response>(TrustTask::for_payload(
request_id.to_string(),
list_payload(),
))
.await
.expect_err("the client must not accept this response")
}
#[tokio::test]
async fn response_with_foreign_thread_id_is_rejected() {
let mut body = well_formed_list_response("urn:uuid:test-thread-binding");
body["threadId"] = serde_json::json!("urn:uuid:some-other-exchange");
match send_against_canned(body, "urn:uuid:test-thread-binding").await {
ClientError::ResponseThreadMismatch { expected, actual } => {
assert_eq!(expected, "urn:uuid:test-thread-binding");
assert_eq!(actual.as_deref(), Some("urn:uuid:some-other-exchange"));
}
other => panic!("expected ResponseThreadMismatch, got {other:?}"),
}
}
#[tokio::test]
async fn response_with_wrong_type_is_rejected() {
let mut body = well_formed_list_response("urn:uuid:test-type-binding");
body["type"] = serde_json::json!("https://trusttasks.org/spec/acl/list/0.1");
match send_against_canned(body, "urn:uuid:test-type-binding").await {
ClientError::ResponseTypeMismatch { expected, actual } => {
assert_eq!(
expected,
"https://trusttasks.org/spec/acl/list/0.1#response"
);
assert_eq!(actual, "https://trusttasks.org/spec/acl/list/0.1");
}
other => panic!("expected ResponseTypeMismatch, got {other:?}"),
}
}
#[tokio::test]
async fn response_from_unexpected_issuer_is_rejected() {
let mut body = well_formed_list_response("urn:uuid:test-issuer-binding");
body["issuer"] = serde_json::json!("did:web:mallory.example");
match send_against_canned(body, "urn:uuid:test-issuer-binding").await {
ClientError::ResponseIssuerMismatch { expected, actual } => {
assert_eq!(expected, SERVER_VID);
assert_eq!(actual.as_deref(), Some("did:web:mallory.example"));
}
other => panic!("expected ResponseIssuerMismatch, got {other:?}"),
}
}
#[tokio::test]
async fn response_addressed_to_someone_else_is_rejected() {
let mut body = well_formed_list_response("urn:uuid:test-recipient-binding");
body["recipient"] = serde_json::json!("did:web:carol.example");
match send_against_canned(body, "urn:uuid:test-recipient-binding").await {
ClientError::ResponseRecipientMismatch { expected, actual } => {
assert_eq!(expected, "did:web:alice.example");
assert_eq!(actual.as_deref(), Some("did:web:carol.example"));
}
other => panic!("expected ResponseRecipientMismatch, got {other:?}"),
}
}
#[tokio::test]
async fn well_formed_response_passes_every_binding_check() {
let body = well_formed_list_response("urn:uuid:test-binding-happy");
let addr = spawn_canned_server(200, body).await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let resp = client
.send::<list::v0_1::Payload, list::v0_1::Response>(TrustTask::for_payload(
"urn:uuid:test-binding-happy",
list_payload(),
))
.await
.unwrap();
assert_eq!(
resp.thread_id.as_deref(),
Some("urn:uuid:test-binding-happy")
);
}
#[tokio::test]
async fn error_response_about_another_document_is_rejected() {
let req = TrustTask::for_payload("urn:uuid:someone-elses-request".to_string(), list_payload());
let error_doc = req.reject_with(
"urn:uuid:canned-error".to_string(),
RejectReason::PermissionDenied {
reason: "nope".into(),
},
);
let body = serde_json::to_value(&error_doc).unwrap();
assert!(
body["payload"]["inResponseTo"]["id"]
== serde_json::json!("urn:uuid:someone-elses-request"),
"fixture must actually carry a foreign inResponseTo.id: {body}"
);
let addr = spawn_canned_server(403, body).await;
let client = build_client(addr, "did:web:alice.example", Some("alice"));
let err = client
.send::<list::v0_1::Payload, list::v0_1::Response>(TrustTask::for_payload(
"urn:uuid:test-error-binding",
list_payload(),
))
.await
.unwrap_err();
match err {
ClientError::ErrorResponseMismatch { expected, actual } => {
assert_eq!(expected, "urn:uuid:test-error-binding");
assert_eq!(actual, "urn:uuid:someone-elses-request");
}
other => panic!("expected ErrorResponseMismatch, got {other:?}"),
}
}
async fn spawn_discovery_server(public: bool) -> SocketAddr {
let mut builder = HttpsServer::builder()
.local_vid(SERVER_VID)
.with_auth(BearerAuth::from_pairs([("alice", "did:web:alice.example")]))
.require_attribution(false)
.on::<list::v0_1::Payload, list::v0_1::Response, _>(|_req, _ctx| {
Ok(list::v0_1::Response {
entries: vec![],
cursor: None,
redacted_fields: vec![],
truncated: false,
ext: None,
})
})
.enable_discovery();
if public {
builder = builder.public_discovery();
}
let server = builder.build();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = server.into_router();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
addr
}
#[tokio::test]
async fn discovery_requires_an_authenticated_sender_by_default() {
let addr = spawn_discovery_server(false).await;
let client = build_client(addr, "did:web:stranger.example", None);
let err = client
.send::<discovery::Payload, discovery::Response>(TrustTask::for_payload(
"urn:uuid:test-discovery-unauth",
discovery::Payload { patterns: vec![] },
))
.await
.unwrap_err();
match err {
ClientError::TrustTaskError { http_status, error } => {
assert_eq!(http_status, 403);
assert_eq!(error.payload.code, StandardCode::PermissionDenied.into());
let msg = error.payload.message.as_deref().unwrap_or("");
assert!(!msg.contains("acl/"), "wire leak (route table): {msg}");
}
other => panic!("expected permissionDenied, got {other:?}"),
}
}
#[tokio::test]
async fn public_discovery_opt_in_answers_unauthenticated_callers() {
let addr = spawn_discovery_server(true).await;
let client = build_client(addr, "did:web:stranger.example", None);
let resp = client
.send::<discovery::Payload, discovery::Response>(TrustTask::for_payload(
"urn:uuid:test-discovery-public",
discovery::Payload { patterns: vec![] },
))
.await
.unwrap();
assert!(!resp.payload.supported_types.is_empty());
}
#[tokio::test]
async fn non_json_content_type_is_rejected_with_415() {
let addr = spawn_server().await;
let url = format!("http://{addr}/trust-tasks");
let document = serde_json::json!({
"id": "urn:uuid:simple-request",
"type": "https://trusttasks.org/spec/acl/list/0.1",
"issuer": "did:web:alice.example",
"recipient": SERVER_VID,
"payload": {}
})
.to_string();
for content_type in ["text/plain", "application/x-www-form-urlencoded"] {
let resp = reqwest::Client::new()
.post(&url)
.header("content-type", content_type)
.body(document.clone())
.send()
.await
.unwrap();
assert_eq!(
resp.status(),
reqwest::StatusCode::UNSUPPORTED_MEDIA_TYPE,
"{content_type} must not reach the dispatch pipeline"
);
}
let resp = reqwest::Client::new()
.post(&url)
.body(document.clone())
.send()
.await
.unwrap();
assert_eq!(
resp.status(),
reqwest::StatusCode::UNSUPPORTED_MEDIA_TYPE,
"an absent Content-Type must not be treated as application/json"
);
let resp = reqwest::Client::new()
.post(&url)
.header("content-type", "application/json; charset=utf-8")
.header("authorization", "Bearer alice")
.body(document)
.send()
.await
.unwrap();
assert_ne!(resp.status(), reqwest::StatusCode::UNSUPPORTED_MEDIA_TYPE);
}
#[tokio::test]
async fn stalled_request_body_is_cut_off_with_408() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
let server = HttpsServer::builder()
.local_vid(SERVER_VID)
.with_auth(BearerAuth::from_pairs([("alice", "did:web:alice.example")]))
.request_timeout(std::time::Duration::from_millis(150))
.on::<list::v0_1::Payload, list::v0_1::Response, _>(|_req, _ctx| {
Ok(list::v0_1::Response {
entries: vec![],
cursor: None,
redacted_fields: vec![],
truncated: false,
ext: None,
})
})
.build();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = server.into_router();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
let mut socket = tokio::net::TcpStream::connect(addr).await.unwrap();
socket
.write_all(
b"POST /trust-tasks HTTP/1.1\r\n\
Host: localhost\r\n\
Content-Type: application/json\r\n\
Authorization: Bearer alice\r\n\
Content-Length: 4096\r\n\
\r\n\
{",
)
.await
.unwrap();
socket.flush().await.unwrap();
let started = std::time::Instant::now();
let mut response = Vec::new();
tokio::time::timeout(
std::time::Duration::from_secs(5),
socket.read_to_end(&mut response),
)
.await
.expect("the server must not hold a stalled connection open indefinitely")
.unwrap();
let head = String::from_utf8_lossy(&response);
assert!(
head.starts_with("HTTP/1.1 408"),
"expected 408 Request Timeout, got: {head:?}"
);
assert!(
started.elapsed() < std::time::Duration::from_secs(5),
"the connection must be released promptly"
);
}
#[test]
fn binding_uri_names_the_current_binding_version() {
assert_eq!(
trust_tasks_https::BINDING_URI,
"https://trusttasks.org/binding/https/0.2"
);
}
struct FailingReplayGuard;
#[async_trait::async_trait]
impl ReplayGuard for FailingReplayGuard {
async fn claim(
&self,
_id: &str,
_digest: &DocumentDigest,
_retain_until: Option<DateTime<Utc>>,
_now: DateTime<Utc>,
) -> Result<ReplayVerdict, ReplayGuardError> {
Err(ReplayGuardError(
"connection refused: redis://replay-store.internal.example:6379".into(),
))
}
}
struct ClaimOnlyGuard(InMemoryReplayGuard);
#[async_trait::async_trait]
impl ReplayGuard for ClaimOnlyGuard {
async fn claim(
&self,
id: &str,
digest: &DocumentDigest,
retain_until: Option<DateTime<Utc>>,
now: DateTime<Utc>,
) -> Result<ReplayVerdict, ReplayGuardError> {
self.0.claim(id, digest, retain_until, now).await
}
async fn record_response(
&self,
id: &str,
_response: Option<&serde_json::Value>,
) -> Result<(), ReplayGuardError> {
self.0.record_response(id, None).await
}
async fn release(&self, id: &str, digest: &DocumentDigest) -> Result<(), ReplayGuardError> {
self.0.release(id, digest).await
}
}
struct InFlightGuard;
#[async_trait::async_trait]
impl ReplayGuard for InFlightGuard {
async fn claim(
&self,
_id: &str,
_digest: &DocumentDigest,
_retain_until: Option<DateTime<Utc>>,
_now: DateTime<Utc>,
) -> Result<ReplayVerdict, ReplayGuardError> {
Ok(ReplayVerdict::Duplicate {
prior_response: None,
in_flight: true,
})
}
}
enum HandlerBehaviour {
Succeed,
RefuseFirst,
}
async fn spawn_replay_server(
configure: impl FnOnce(
trust_tasks_https::HttpsServerBuilder,
) -> trust_tasks_https::HttpsServerBuilder,
behaviour: HandlerBehaviour,
) -> (SocketAddr, Arc<AtomicUsize>) {
let calls = Arc::new(AtomicUsize::new(0));
let counter = Arc::clone(&calls);
let builder = HttpsServer::builder()
.local_vid(SERVER_VID)
.with_auth(BearerAuth::from_pairs([("alice", "did:web:alice.example")]))
.on::<list::v0_1::Payload, list::v0_1::Response, _>(move |_req, _ctx| {
let n = counter.fetch_add(1, AtomicOrdering::SeqCst);
match &behaviour {
HandlerBehaviour::Succeed => {}
HandlerBehaviour::RefuseFirst if n == 0 => {
return Err(RejectReason::PermissionDenied {
reason: "first attempt refused".into(),
});
}
HandlerBehaviour::RefuseFirst => {}
}
Ok(list::v0_1::Response {
entries: vec![],
cursor: None,
redacted_fields: vec![],
truncated: false,
ext: None,
})
});
let server = configure(builder).build();
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let app = server.into_router();
tokio::spawn(async move {
axum::serve(listener, app).await.unwrap();
});
(addr, calls)
}
fn replay_body(id: &str, issued_at: DateTime<Utc>, page_size: Option<u32>) -> String {
let mut payload = serde_json::Map::new();
if let Some(n) = page_size {
payload.insert("pageSize".into(), serde_json::json!(n));
}
serde_json::json!({
"id": id,
"type": "https://trusttasks.org/spec/acl/list/0.1",
"issuer": "did:web:alice.example",
"recipient": SERVER_VID,
"issuedAt": issued_at.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
"payload": serde_json::Value::Object(payload),
})
.to_string()
}
async fn post_raw(addr: SocketAddr, body: &str) -> (u16, String) {
let resp = reqwest::Client::new()
.post(format!("http://{addr}/trust-tasks"))
.header("authorization", "Bearer alice")
.header("content-type", "application/json")
.body(body.to_string())
.send()
.await
.unwrap();
let status = resp.status().as_u16();
(status, resp.text().await.unwrap())
}
#[tokio::test]
async fn identical_resend_does_not_dispatch_the_handler_twice() {
let (addr, calls) = spawn_replay_server(|b| b, HandlerBehaviour::Succeed).await;
let body = replay_body("urn:uuid:replay-once", Utc::now(), None);
let (first_status, _) = post_raw(addr, &body).await;
let (second_status, _) = post_raw(addr, &body).await;
assert_eq!(first_status, 200);
assert_eq!(second_status, 200, "a duplicate is not an error (§7.2)");
assert_eq!(
calls.load(AtomicOrdering::SeqCst),
1,
"the consequential effect MUST NOT happen twice"
);
}
#[tokio::test]
async fn duplicate_is_answered_with_the_recorded_response() {
let (addr, _calls) = spawn_replay_server(|b| b, HandlerBehaviour::Succeed).await;
let body = replay_body("urn:uuid:replay-recorded", Utc::now(), None);
let (_, first) = post_raw(addr, &body).await;
let (status, second) = post_raw(addr, &body).await;
assert_eq!(status, 200);
assert_eq!(
second, first,
"the duplicate must be answered with the response the first execution produced"
);
let doc: serde_json::Value = serde_json::from_str(&second).unwrap();
assert_eq!(
doc["type"],
serde_json::json!("https://trusttasks.org/spec/acl/list/0.1#response")
);
}
#[tokio::test]
async fn different_document_under_a_reused_id_is_an_id_conflict() {
let (addr, calls) = spawn_replay_server(|b| b, HandlerBehaviour::Succeed).await;
let issued = Utc::now();
let original = replay_body("urn:uuid:replay-conflict", issued, None);
let altered = replay_body("urn:uuid:replay-conflict", issued, Some(50));
let (first_status, _) = post_raw(addr, &original).await;
let (status, body) = post_raw(addr, &altered).await;
assert_eq!(first_status, 200);
assert_eq!(status, 409);
let doc: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(doc["payload"]["code"], serde_json::json!("idConflict"));
assert_eq!(
calls.load(AtomicOrdering::SeqCst),
1,
"the conflicting document MUST NOT be executed"
);
}
#[tokio::test]
async fn guard_error_fails_closed_to_unavailable_and_does_not_dispatch() {
let (addr, calls) = spawn_replay_server(
|b| b.with_replay_guard(FailingReplayGuard),
HandlerBehaviour::Succeed,
)
.await;
let (status, body) =
post_raw(addr, &replay_body("urn:uuid:guard-down", Utc::now(), None)).await;
assert_eq!(status, 503);
let doc: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(doc["payload"]["code"], serde_json::json!("unavailable"));
assert_eq!(doc["payload"]["retryable"], serde_json::json!(true));
assert_eq!(
calls.load(AtomicOrdering::SeqCst),
0,
"a guard error MUST NOT be executed through"
);
let message = doc["payload"]["message"].as_str().unwrap_or("");
assert!(!message.contains("redis"), "wire leak: {message}");
assert!(
!message.contains("replay-store.internal.example"),
"wire leak: {message}"
);
}
#[tokio::test]
async fn duplicate_with_no_recorded_response_is_silence_not_a_failure() {
let (addr, calls) = spawn_replay_server(
|b| b.with_replay_guard(ClaimOnlyGuard(InMemoryReplayGuard::new(16))),
HandlerBehaviour::Succeed,
)
.await;
let body = replay_body("urn:uuid:replay-silent", Utc::now(), None);
let (first_status, _) = post_raw(addr, &body).await;
let (status, second) = post_raw(addr, &body).await;
assert_eq!(first_status, 200);
assert_eq!(status, 204, "silence, not a failure");
assert!(second.is_empty());
assert_eq!(calls.load(AtomicOrdering::SeqCst), 1);
}
#[tokio::test]
async fn in_flight_duplicate_is_accepted_not_re_executed() {
let (addr, calls) = spawn_replay_server(
|b| b.with_replay_guard(InFlightGuard),
HandlerBehaviour::Succeed,
)
.await;
let (status, body) = post_raw(
addr,
&replay_body("urn:uuid:replay-in-flight", Utc::now(), None),
)
.await;
assert_eq!(status, 202, "existing execution state, not a second one");
assert!(body.is_empty(), "202 carries no result document");
assert_eq!(
calls.load(AtomicOrdering::SeqCst),
0,
"an in-flight duplicate MUST NOT begin another execution"
);
}
#[tokio::test]
async fn a_refused_dispatch_releases_the_claim() {
let (addr, calls) = spawn_replay_server(|b| b, HandlerBehaviour::RefuseFirst).await;
let body = replay_body("urn:uuid:replay-released", Utc::now(), None);
let (first_status, _) = post_raw(addr, &body).await;
let (second_status, _) = post_raw(addr, &body).await;
assert_eq!(first_status, 403, "the handler refused this one");
assert_eq!(
second_status, 200,
"the resend must be re-evaluated, not absorbed as a duplicate of a refusal"
);
assert_eq!(calls.load(AtomicOrdering::SeqCst), 2);
}
#[tokio::test]
async fn replay_protection_false_restores_the_undefended_path() {
let (addr, calls) =
spawn_replay_server(|b| b.replay_protection(false), HandlerBehaviour::Succeed).await;
let body = replay_body("urn:uuid:replay-optout", Utc::now(), None);
post_raw(addr, &body).await;
post_raw(addr, &body).await;
assert_eq!(
calls.load(AtomicOrdering::SeqCst),
2,
"opting out means opting out"
);
}
#[tokio::test]
async fn a_stale_document_is_refused_and_never_dispatched() {
let (addr, calls) = spawn_replay_server(|b| b, HandlerBehaviour::Succeed).await;
let long_ago = Utc::now() - ChronoDuration::hours(2);
let (status, body) = post_raw(addr, &replay_body("urn:uuid:stale", long_ago, None)).await;
assert_eq!(status, 422);
let doc: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(doc["payload"]["code"], serde_json::json!("expired"));
assert_eq!(calls.load(AtomicOrdering::SeqCst), 0);
let message = doc["payload"]["message"].as_str().unwrap_or("");
assert!(!message.contains("2 hours"), "wire leak: {message}");
}
#[tokio::test]
async fn a_future_dated_document_is_refused_and_never_dispatched() {
let (addr, calls) = spawn_replay_server(|b| b, HandlerBehaviour::Succeed).await;
let tomorrow = Utc::now() + ChronoDuration::days(1);
let (status, body) = post_raw(addr, &replay_body("urn:uuid:future", tomorrow, None)).await;
assert_eq!(status, 400);
let doc: serde_json::Value = serde_json::from_str(&body).unwrap();
assert_eq!(
doc["payload"]["code"],
serde_json::json!("malformedRequest")
);
assert_eq!(calls.load(AtomicOrdering::SeqCst), 0);
}
#[tokio::test]
async fn a_widened_window_accepts_what_the_default_refuses() {
let (addr, calls) = spawn_replay_server(
|b| b.freshness(FreshnessPolicy::consequential().with_max_age(ChronoDuration::hours(6))),
HandlerBehaviour::Succeed,
)
.await;
let long_ago = Utc::now() - ChronoDuration::hours(2);
let (status, _) = post_raw(addr, &replay_body("urn:uuid:widened", long_ago, None)).await;
assert_eq!(status, 200);
assert_eq!(calls.load(AtomicOrdering::SeqCst), 1);
}
#[tokio::test]
async fn payload_deserialisation_failure_does_not_leak_the_serde_path() {
let (addr, _calls) = spawn_replay_server(|b| b, HandlerBehaviour::Succeed).await;
let body = serde_json::json!({
"id": "urn:uuid:bad-payload",
"type": "https://trusttasks.org/spec/acl/list/0.1",
"issuer": "did:web:alice.example",
"recipient": SERVER_VID,
"issuedAt": Utc::now().to_rfc3339(),
"payload": { "pageSize": "not-a-number" },
})
.to_string();
let (status, response) = post_raw(addr, &body).await;
assert_eq!(status, 400);
let doc: serde_json::Value = serde_json::from_str(&response).unwrap();
assert_eq!(
doc["payload"]["code"],
serde_json::json!("malformedRequest")
);
let message = doc["payload"]["message"].as_str().unwrap_or("");
for leak in ["pageSize", "not-a-number", "line 1", "column"] {
assert!(
!message.contains(leak),
"serde detail reached the wire ({leak:?}): {message}"
);
}
}
#[tokio::test]
async fn document_parse_failure_does_not_leak_the_serde_path() {
let (addr, _calls) = spawn_replay_server(|b| b, HandlerBehaviour::Succeed).await;
let (status, response) = post_raw(addr, r#"{"id": 42}"#).await;
assert_eq!(status, 400);
let doc: serde_json::Value = serde_json::from_str(&response).unwrap();
let message = doc["payload"]["message"].as_str().unwrap_or("");
for leak in ["line 1", "column", "invalid type"] {
assert!(
!message.contains(leak),
"serde detail reached the wire ({leak:?}): {message}"
);
}
}