use serde_json::{Value, json};
use did_hosting_common::did_ops::{AgentNameEntry, DidRecord, content_log_key, did_key};
use did_hosting_common::server::acl::Role;
use did_hosting_common::server::trust_tasks::ProofRule;
use super::TASKS;
use super::harness::*;
use crate::server::AppState;
fn did_signed_rows() -> impl Iterator<Item = &'static (&'static str, ProofRule)> {
TASKS
.iter()
.filter(|(_, rule)| !matches!(rule, ProofRule::BoundKey(_)))
}
fn is_task_code(code: &str) -> bool {
code.contains(':')
}
fn is_served(reply: &Value) -> bool {
let reply_type = reply["type"].as_str().unwrap_or_default();
if reply_type.ends_with("#response") {
return true;
}
let code = reply["payload"]["code"].as_str().unwrap_or_default();
let message = reply["payload"]["message"].as_str().unwrap_or_default();
!matches!(
code,
"proofRequired" | "proofInvalid" | "unsupportedType" | "idConflict" | "wrongRecipient"
) && !(code == "permissionDenied" && message.contains("not present in the maintainer's ACL"))
&& !(code == "malformedRequest" && message.contains("payload"))
}
async fn seed_published(state: &AppState, owner: &str, mnemonic: &str) {
let mut record = seed_did(state, owner, mnemonic).await;
record.version_count = 2;
state
.dids_ks
.insert(did_key(mnemonic), &record)
.await
.unwrap();
state
.dids_ks
.insert_raw(
content_log_key(mnemonic),
b"{\"versionId\":\"1-a\",\"state\":{},\"parameters\":{}}\n{\"versionId\":\"2-b\",\"state\":{},\"parameters\":{}}".to_vec(),
)
.await
.unwrap();
}
fn assertion() -> Value {
json!({
"id": "AAAA",
"rawId": "AAAA",
"type": "public-key",
"response": {
"authenticatorData": "AAAA",
"clientDataJSON": "AAAA",
"signature": "AAAA",
},
})
}
fn attestation() -> Value {
json!({
"id": "AAAA",
"rawId": "AAAA",
"type": "public-key",
"response": { "attestationObject": "AAAA", "clientDataJSON": "AAAA" },
})
}
async fn sample(state: &AppState, admin: &Caller, n: usize, type_uri: &str) -> Value {
let slot = format!("slot-{n}");
let slug = type_uri.trim_start_matches("https://trusttasks.org/spec/");
match slug {
"did-management/did/check-name/0.1" => {
json!({ "path": format!("free-{n}"), "reserve": false })
}
"did-management/did/register/0.1" => {
json!({ "path": format!("reg-{n}"), "method": "webvh", "didData": "not a log", "force": false })
}
"did-management/did/info/0.1"
| "did-management/did/delete/0.1"
| "did-management/agent-name/list/0.1" => {
seed_did(state, &admin.did, &slot).await;
json!({ "mnemonic": slot })
}
"did-management/did/list/0.1"
| "did-management/me/domains/0.1"
| "did-management/domain/list/0.1"
| "did-management/registry/list/0.1"
| "did-management/server/info/0.1"
| "did-management/server/config/0.1"
| "did-management/server/metrics/0.1"
| "did-management/stats/get/0.1"
| "did-management/identity/list/0.1"
| "auth/passkey/login/start/0.2"
| "auth/passkey/enroll/invite/list/0.1"
| "auth/passkey/enroll/start/0.2" => json!({}),
"auth/passkey/enroll/invite/0.2" => {
json!({ "subject": format!("did:example:invitee-{n}") })
}
"auth/passkey/enroll/redeem/start/0.1" => {
json!({ "token": "inv_no-such-invite-token", "claimCode": "0000-0000-0000" })
}
"auth/passkey/enroll/redeem/finish/0.1" | "auth/passkey/enroll/finish/0.2" => {
json!({ "enrollmentId": "no-such-enrolment", "credential": attestation() })
}
"did-management/did/change-owner/0.1" => {
seed_did(state, &admin.did, &slot).await;
let heir = member(state, 100 + (n % 150) as u8, Role::Owner).await;
json!({ "mnemonic": slot, "newOwner": heir.did })
}
"did-management/did/set-state/0.1" => {
seed_did(state, &admin.did, &slot).await;
json!({ "mnemonic": slot, "state": "suspended" })
}
"did-management/did/rollback/0.1" => {
seed_published(state, &admin.did, &slot).await;
json!({ "mnemonic": slot, "targetVersion": 1 })
}
"did-management/did/log/0.1" => {
seed_published(state, &admin.did, &slot).await;
json!({ "mnemonic": slot, "raw": true })
}
"webvh/witness/publish/0.1" => {
seed_did(state, &admin.did, &slot).await;
json!({ "mnemonic": slot, "witness": {} })
}
"did-management/agent-name/update/0.1" => {
seed_did(state, &admin.did, &slot).await;
json!({ "mnemonic": slot, "name": "alice", "state": "active", "didData": "not a log" })
}
"did-management/agent-name/remove/0.1" => {
seed_did(state, &admin.did, &slot).await;
json!({ "mnemonic": slot, "name": "alice", "didData": "not a log" })
}
"did-management/agent-name/check/0.1" => {
json!({ "name": "alice", "domain": "example.com" })
}
"did-management/agent-name/resolve/0.1" => {
json!({ "dids": [format!("did:webvh:abc:control.test:{slot}")] })
}
"did-management/domain/create/0.1" => {
json!({ "name": format!("d{n}.example.com"), "setAsDefault": false })
}
"did-management/domain/update/0.1" | "did-management/domain/set-default/0.1" => {
json!({ "name": "nowhere.example.com" })
}
"did-management/domain/set-state/0.1" => {
json!({ "name": "nowhere.example.com", "state": "disabled" })
}
"did-management/domain/purge/0.1" => {
json!({ "name": "nowhere.example.com", "purgeServers": false })
}
"did-management/domain/assign/0.1"
| "did-management/domain/unassign/0.1"
| "did-management/registry/purge-domain/0.1" => {
json!({ "domain": "nowhere.example.com", "instanceId": "nobody" })
}
"did-management/registry/get/0.1"
| "did-management/registry/check/0.1"
| "did-management/registry/deregister/0.1" => json!({ "instanceId": "nobody" }),
"did-management/registry/admin-register/0.1" => json!({
"instanceId": format!("edge-{n}"),
"did": format!("did:example:edge-{n}"),
"publicUrl": format!("https://edge-{n}.example.com"),
}),
"did-management/stats/timeseries/0.1" => json!({ "range": "lastHour" }),
"did-management/identity/retire/0.1" => json!({ "generationId": 999 }),
"auth/step-up/start/0.1" => json!({ "sessionId": "no-such-session" }),
"auth/step-up/approve-response/0.5" => json!({
"challenge": "0123456789abcdef0123456789abcdef",
"decision": "approved",
"subject": admin.did,
"sessionId": "no-such-session",
}),
"auth/revoke-session/0.2" => json!({ "sessionId": "no-such-session" }),
"auth/passkey/login/finish/0.2" => {
json!({ "authId": "no-such-ceremony", "credential": assertion() })
}
"auth/passkey/enroll/invite/update/0.1" => {
json!({ "inviteId": "no-such-invite", "role": "owner" })
}
"auth/passkey/enroll/invite/revoke/0.1" => json!({ "inviteId": "no-such-invite" }),
"auth/passkey/list/0.1" => json!({}),
"auth/passkey/admin-list/0.1" => json!({ "subject": admin.did, "purpose": "session" }),
"auth/passkey/revoke/start/0.2" => json!({ "credentialId": "AAAA" }),
"auth/passkey/revoke/finish/0.2" => {
json!({ "revocationId": "no-such-revocation", "uvCredential": assertion() })
}
other => panic!("no sample for {other}: add one when adding a row"),
}
}
#[tokio::test]
async fn every_row_is_served_on_every_transport() {
let (state, _dir) = state().await;
let admin = member(&state, 1, Role::Admin).await;
let mut n = 0;
for (type_uri, _) in did_signed_rows() {
for via in VIAS {
n += 1;
let payload = sample(&state, &admin, n, type_uri).await;
let reply = call(&state, via, &admin, type_uri, payload).await;
assert!(
is_served(&reply),
"{via:?} {type_uri} was not served: {reply}"
);
if reply["type"] == format!("{type_uri}#response") {
conforms(&reply);
assert_eq!(reply["issuer"], CONTROL, "{via:?} {type_uri}: {reply}");
assert!(
reply["proof"].is_object(),
"{via:?} {type_uri}: reply is signed"
);
}
}
}
}
#[tokio::test]
async fn an_unsigned_request_is_refused_where_a_proof_is_required() {
let (state, _dir) = state().await;
let admin = member(&state, 2, Role::Admin).await;
let mut n = 1000;
for (type_uri, rule) in did_signed_rows() {
if *rule == ProofRule::Optional {
continue;
}
for via in VIAS {
n += 1;
let payload = sample(&state, &admin, n, type_uri).await;
let reply = send(&state, via, &admin, request(type_uri, &admin.did, payload)).await;
assert_eq!(code(&reply), "proofRequired", "{via:?} {type_uri}: {reply}");
}
}
}
#[tokio::test]
async fn a_proof_under_the_wrong_key_relationship_is_refused() {
let (state, _dir) = state().await;
let admin = member(&state, 3, Role::Admin).await;
let mut n = 2000;
for (type_uri, rule) in did_signed_rows() {
if *rule == ProofRule::Optional {
continue;
}
for via in VIAS {
n += 1;
let payload = sample(&state, &admin, n, type_uri).await;
let doc = request(type_uri, &admin.did, payload);
let doc = match rule {
ProofRule::AssertionMethod => signed(doc, &admin.key).await,
_ => signed_as_assertion(doc, &admin.key).await,
};
let reply = send(&state, via, &admin, doc).await;
assert_eq!(code(&reply), "proofInvalid", "{via:?} {type_uri}: {reply}");
}
}
}
#[tokio::test]
async fn a_signer_outside_the_acl_is_refused_on_every_transport() {
let (state, _dir) = state().await;
let admin = member(&state, 4, Role::Admin).await;
let outsider = stranger(5);
let mut n = 3000;
for (type_uri, rule) in did_signed_rows() {
if matches!(rule, ProofRule::Optional | ProofRule::SessionKey) {
continue;
}
for via in VIAS {
n += 1;
let payload = sample(&state, &admin, n, type_uri).await;
let reply = call(&state, via, &outsider, type_uri, payload).await;
assert_eq!(
code(&reply),
"permissionDenied",
"{via:?} {type_uri}: {reply}"
);
}
}
}
#[tokio::test]
async fn an_optional_proof_that_does_not_verify_is_refused() {
let (state, _dir) = state().await;
let admin = member(&state, 6, Role::Admin).await;
for (type_uri, rule) in TASKS {
if *rule != ProofRule::Optional {
continue;
}
for via in VIAS {
let mut doc = signed(request(type_uri, &admin.did, json!({})), &admin.key).await;
doc["id"] = json!(format!("urn:uuid:{}", uuid::Uuid::new_v4()));
let reply = send(&state, via, &admin, doc).await;
assert!(
matches!(code(&reply).as_str(), "proofInvalid" | "permissionDenied"),
"{via:?} {type_uri}: {reply}"
);
}
}
}
const LIST: &str = "https://trusttasks.org/spec/did-management/did/list/0.1";
const INFO: &str = "https://trusttasks.org/spec/did-management/did/info/0.1";
const CHECK_NAME: &str = "https://trusttasks.org/spec/did-management/did/check-name/0.1";
const DELETE: &str = "https://trusttasks.org/spec/did-management/did/delete/0.1";
const CHANGE_OWNER: &str = "https://trusttasks.org/spec/did-management/did/change-owner/0.1";
const WITNESS: &str = "https://trusttasks.org/spec/webvh/witness/publish/0.1";
const NAME_UPDATE: &str = "https://trusttasks.org/spec/did-management/agent-name/update/0.1";
const NAME_REMOVE: &str = "https://trusttasks.org/spec/did-management/agent-name/remove/0.1";
const NAME_LIST: &str = "https://trusttasks.org/spec/did-management/agent-name/list/0.1";
const NAME_CHECK: &str = "https://trusttasks.org/spec/did-management/agent-name/check/0.1";
#[tokio::test]
async fn an_unserved_type_is_refused_as_unsupported() {
let (state, _dir) = state().await;
let owner = member(&state, 10, Role::Owner).await;
let reply = call(
&state,
Via::Tsp,
&owner,
"https://trusttasks.org/spec/did-management/not-a-task/0.1",
json!({}),
)
.await;
assert_eq!(code(&reply), "unsupportedType", "{reply}");
}
#[tokio::test]
async fn an_unknown_payload_member_is_refused() {
let (state, _dir) = state().await;
let owner = member(&state, 11, Role::Owner).await;
for payload in [
json!({ "mnemonic": "alpha", "surprise": true }),
json!({ "mnemonic": "alpha", "new_owner": "did:example:x" }),
] {
let reply = call(&state, Via::Didcomm, &owner, CHANGE_OWNER, payload).await;
assert_eq!(code(&reply), "malformedRequest", "{reply}");
}
let reply = call(&state, Via::Didcomm, &owner, INFO, json!({})).await;
assert_eq!(
code(&reply),
"malformedRequest",
"a missing mnemonic: {reply}"
);
}
#[tokio::test]
async fn list_answers_records_and_total_scoped_to_the_caller() {
let (state, _dir) = state().await;
let owner = member(&state, 12, Role::Owner).await;
seed_did(&state, &owner.did, "alpha-beta").await;
seed_did(&state, &owner.did, "gamma-delta").await;
seed_did(&state, "did:example:other", "eta-theta").await;
let reply = call(&state, Via::Tsp, &owner, LIST, json!({})).await;
conforms(&reply);
let body = ok(&reply, LIST);
assert_eq!(body["total"], 2, "{body}");
let mnemonics: Vec<&str> = body["records"]
.as_array()
.unwrap()
.iter()
.map(|r| r["mnemonic"].as_str().unwrap())
.collect();
assert_eq!(mnemonics, ["alpha-beta", "gamma-delta"]);
assert_eq!(body["records"][0]["versionCount"], 1);
assert_eq!(body["records"][0]["totalResolves"], 0);
assert_eq!(
body["records"][0]["didUrl"],
"http://control.test/alpha-beta/did.jsonl"
);
let reply = call(
&state,
Via::Tsp,
&owner,
LIST,
json!({ "limit": 1, "offset": 1 }),
)
.await;
let body = ok(&reply, LIST);
assert_eq!(body["total"], 2);
assert_eq!(body["records"].as_array().unwrap().len(), 1);
assert_eq!(body["records"][0]["mnemonic"], "gamma-delta");
}
#[tokio::test]
async fn list_does_not_leak_through_an_owner_prefix() {
let (state, _dir) = state().await;
let owner = member(&state, 13, Role::Owner).await;
let longer = format!("{}:server", owner.did);
seed_did(&state, &owner.did, "short-mn").await;
seed_did(&state, &longer, "long-mn").await;
let body = ok(&call(&state, Via::Tsp, &owner, LIST, json!({})).await, LIST);
assert_eq!(body["total"], 1, "{body}");
assert_eq!(body["records"][0]["mnemonic"], "short-mn");
}
#[tokio::test]
async fn list_owner_filter_is_admin_only() {
let (state, _dir) = state().await;
let owner = member(&state, 14, Role::Owner).await;
let admin = member(&state, 15, Role::Admin).await;
seed_did(&state, "did:example:owner-a", "alpha-beta").await;
seed_did(&state, "did:example:owner-b", "gamma-delta").await;
let reply = call(
&state,
Via::Tsp,
&owner,
LIST,
json!({ "owner": "did:example:owner-a" }),
)
.await;
assert_eq!(code(&reply), "did-management/did/list:forbidden", "{reply}");
let body = ok(&call(&state, Via::Tsp, &admin, LIST, json!({})).await, LIST);
assert_eq!(body["total"], 2);
let body = ok(
&call(
&state,
Via::Tsp,
&admin,
LIST,
json!({ "owner": "did:example:owner-b" }),
)
.await,
LIST,
);
assert_eq!(body["total"], 1);
assert_eq!(body["records"][0]["mnemonic"], "gamma-delta");
}
#[tokio::test]
async fn list_refuses_an_unknown_domain() {
let (state, _dir) = state().await;
let owner = member(&state, 16, Role::Owner).await;
let reply = call(
&state,
Via::Tsp,
&owner,
LIST,
json!({ "domain": "nowhere.example" }),
)
.await;
assert_eq!(code(&reply), "did-management:unknownDomain", "{reply}");
}
#[tokio::test]
async fn info_answers_the_record_to_its_owner_and_to_an_admin_only() {
let (state, _dir) = state().await;
let owner = member(&state, 17, Role::Owner).await;
let attacker = member(&state, 18, Role::Owner).await;
let admin = member(&state, 19, Role::Admin).await;
seed_did(&state, &owner.did, "alpha-beta").await;
let reply = call(
&state,
Via::Tsp,
&attacker,
INFO,
json!({ "mnemonic": "alpha-beta" }),
)
.await;
assert_eq!(code(&reply), "permissionDenied", "{reply}");
for who in [&owner, &admin] {
let reply = call(
&state,
Via::Tsp,
who,
INFO,
json!({ "mnemonic": "alpha-beta" }),
)
.await;
conforms(&reply);
let body = ok(&reply, INFO);
assert_eq!(body["record"]["mnemonic"], "alpha-beta");
assert_eq!(body["record"]["owner"], owner.did.as_str());
assert_eq!(body["record"]["createdAt"], "1970-01-01T00:00:01Z");
}
let reply = call(
&state,
Via::Tsp,
&owner,
INFO,
json!({ "mnemonic": "ghost-token" }),
)
.await;
assert_eq!(code(&reply), "did-management/did/info:notFound", "{reply}");
}
#[tokio::test]
async fn a_record_carries_its_agent_names_under_the_host_extension() {
let (state, _dir) = state().await;
let owner = member(&state, 20, Role::Owner).await;
let mut record = seed_did(&state, &owner.did, "alpha-beta").await;
record.agent_names = vec![AgentNameEntry {
name: "alice".into(),
enabled: true,
created_at: 7,
}];
state
.dids_ks
.insert(did_key("alpha-beta"), &record)
.await
.unwrap();
let reply = call(
&state,
Via::Tsp,
&owner,
INFO,
json!({ "mnemonic": "alpha-beta" }),
)
.await;
conforms(&reply);
let body = ok(&reply, INFO);
assert!(body["record"].get("agentNames").is_none(), "{body}");
assert_eq!(
body["record"]["ext"]["vnd.affinidi.webvh"]["agentNames"][0]["name"],
"alice"
);
}
#[tokio::test]
async fn delete_answers_the_record_as_it_stood() {
let (state, _dir) = state().await;
let owner = member(&state, 21, Role::Owner).await;
seed_did(&state, &owner.did, "alpha-beta").await;
let reply = call(
&state,
Via::Tsp,
&owner,
DELETE,
json!({ "mnemonic": "alpha-beta" }),
)
.await;
conforms(&reply);
assert_eq!(ok(&reply, DELETE)["record"]["mnemonic"], "alpha-beta");
assert!(
state
.dids_ks
.get::<DidRecord>(did_key("alpha-beta"))
.await
.unwrap()
.is_none()
);
let reply = call(
&state,
Via::Tsp,
&owner,
DELETE,
json!({ "mnemonic": "alpha-beta" }),
)
.await;
assert!(
is_task_code(&code(&reply)) || code(&reply) == "taskFailed",
"{reply}"
);
}
#[tokio::test]
async fn check_name_probe_is_read_only_and_reserve_claims_a_slot() {
let (state, _dir) = state().await;
let owner = member(&state, 22, Role::Owner).await;
let reply = call(
&state,
Via::Tsp,
&owner,
CHECK_NAME,
json!({ "path": "free-path" }),
)
.await;
conforms(&reply);
let body = ok(&reply, CHECK_NAME);
assert_eq!(body["available"], true);
assert_eq!(body["reserved"], false);
assert!(body.get("record").is_none());
assert!(
state
.dids_ks
.get::<DidRecord>(did_key("free-path"))
.await
.unwrap()
.is_none(),
"a probe reserves nothing"
);
let reply = call(&state, Via::Tsp, &owner, CHECK_NAME, json!({})).await;
assert_eq!(
code(&reply),
"did-management/did/check-name:invalidPath",
"{reply}"
);
let mut minted = Vec::new();
for _ in 0..2 {
let reply = call(
&state,
Via::Tsp,
&owner,
CHECK_NAME,
json!({ "reserve": true }),
)
.await;
conforms(&reply);
let body = ok(&reply, CHECK_NAME);
assert_eq!(body["available"], true);
assert_eq!(body["reserved"], true);
assert_eq!(body["record"]["versionCount"], 0);
let mnemonic = body["record"]["mnemonic"].as_str().unwrap().to_string();
assert!(
body["record"]["didUrl"]
.as_str()
.unwrap()
.ends_with(&format!("/{mnemonic}/did.jsonl"))
);
let record: DidRecord = state
.dids_ks
.get(did_key(&mnemonic))
.await
.unwrap()
.unwrap();
assert_eq!(record.owner, owner.did);
minted.push(mnemonic);
}
assert_ne!(minted[0], minted[1]);
seed_did(&state, "did:example:owner-b", "taken-path").await;
let reply = call(
&state,
Via::Tsp,
&owner,
CHECK_NAME,
json!({ "path": "taken-path", "reserve": true }),
)
.await;
let body = ok(&reply, CHECK_NAME);
assert_eq!(body["available"], false);
assert_eq!(body["reserved"], false);
}
#[tokio::test]
async fn check_name_keeps_well_known_for_admins() {
let (state, _dir) = state().await;
let owner = member(&state, 23, Role::Owner).await;
let reply = call(
&state,
Via::Tsp,
&owner,
CHECK_NAME,
json!({ "path": ".well-known", "reserve": true }),
)
.await;
assert_eq!(code(&reply), "permissionDenied", "{reply}");
}
#[tokio::test]
async fn change_owner_moves_the_slot_to_an_acl_member_only() {
let (state, _dir) = state().await;
let owner = member(&state, 24, Role::Owner).await;
let heir = member(&state, 25, Role::Owner).await;
let attacker = member(&state, 26, Role::Owner).await;
seed_did(&state, &owner.did, "alpha-beta").await;
let reply = call(
&state,
Via::Tsp,
&attacker,
CHANGE_OWNER,
json!({ "mnemonic": "alpha-beta", "newOwner": heir.did }),
)
.await;
assert_eq!(
code(&reply),
"did-management/did/change-owner:notOwner",
"{reply}"
);
let reply = call(
&state,
Via::Tsp,
&owner,
CHANGE_OWNER,
json!({ "mnemonic": "alpha-beta", "newOwner": "did:example:not-in-acl" }),
)
.await;
assert!(!code(&reply).is_empty(), "{reply}");
let reply = call(
&state,
Via::Tsp,
&owner,
CHANGE_OWNER,
json!({ "mnemonic": "alpha-beta", "newOwner": heir.did }),
)
.await;
conforms(&reply);
assert_eq!(
ok(&reply, CHANGE_OWNER)["record"]["owner"],
heir.did.as_str()
);
assert!(
state
.dids_ks
.prefix_iter_raw(format!("owner:{}:", owner.did))
.await
.unwrap()
.is_empty()
);
assert_eq!(
state
.dids_ks
.prefix_iter_raw(format!("owner:{}:", heir.did))
.await
.unwrap()
.len(),
1
);
}
async fn seed_witnessed_genesis(
state: &AppState,
owner: &str,
mnemonic: &str,
) -> (String, affinidi_secrets_resolver::secrets::Secret) {
use didwebvh_rs::parameters::Parameters;
use didwebvh_rs::witness::{Witness, Witnesses};
use didwebvh_rs::{DIDWebVHState, Multibase};
let mut key = affinidi_secrets_resolver::secrets::Secret::generate_ed25519(None, None);
let pk = key.get_public_keymultibase().expect("update key multibase");
key.id = format!("did:key:{pk}#{pk}");
let mut witness_key = affinidi_secrets_resolver::secrets::Secret::generate_ed25519(None, None);
let witness_pk = witness_key
.get_public_keymultibase()
.expect("witness key multibase");
witness_key.id = format!("did:key:{witness_pk}#{witness_pk}");
let witness_did = format!("did:key:{witness_pk}");
let params = Parameters {
update_keys: Some(std::sync::Arc::new(vec![Multibase::new(pk)])),
portable: Some(false),
witness: Some(std::sync::Arc::new(Witnesses::Value {
threshold: 1,
witnesses: vec![Witness {
id: Multibase::new(witness_did),
}],
})),
..Default::default()
};
let did_id = format!("did:webvh:{{SCID}}:control.test:{mnemonic}");
let doc = json!({
"id": &did_id,
"@context": ["https://www.w3.org/ns/did/v1"],
"verificationMethod": [{
"id": format!("{did_id}#key-0"),
"type": "Multikey",
"publicKeyMultibase": key.get_public_keymultibase().unwrap(),
"controller": &did_id,
}],
"authentication": [format!("{did_id}#key-0")],
"assertionMethod": [format!("{did_id}#key-0")],
});
let mut vh_state = DIDWebVHState::default();
vh_state
.create_log_entry(None, &doc, ¶ms, &key)
.await
.expect("genesis entry");
let version_id = vh_state
.log_entries()
.last()
.unwrap()
.get_version_id()
.to_string();
let jsonl = vh_state
.log_entries()
.iter()
.map(|e| serde_json::to_string(&e.log_entry).unwrap())
.collect::<Vec<_>>()
.join("\n");
seed_did(state, owner, mnemonic).await;
state
.dids_ks
.insert_raw(content_log_key(mnemonic), jsonl.into_bytes())
.await
.unwrap();
(version_id, witness_key)
}
#[tokio::test]
async fn witness_publish_stores_a_proof_that_verifies_against_the_log() {
let (state, _dir) = state().await;
let owner = member(&state, 27, Role::Owner).await;
let (version_id, witness_key) = seed_witnessed_genesis(&state, &owner.did, "alpha-beta").await;
let proof = affinidi_data_integrity::DataIntegrityProof::sign(
&json!({ "versionId": &version_id }),
&witness_key,
affinidi_data_integrity::SignOptions::new(),
)
.await
.expect("sign witness proof");
let reply = call(
&state,
Via::Tsp,
&owner,
WITNESS,
json!({
"mnemonic": "alpha-beta",
"witness": {
"versionId": version_id,
"witness": witness_key.id.split('#').next().unwrap(),
"proof": serde_json::to_value(&proof).unwrap(),
},
}),
)
.await;
conforms(&reply);
assert_eq!(
ok(&reply, WITNESS)["witnessUrl"],
"http://control.test/alpha-beta/did-witness.json"
);
}
#[tokio::test]
async fn witness_publish_rejects_a_proof_from_a_key_the_log_does_not_name_as_witness() {
let (state, _dir) = state().await;
let owner = member(&state, 28, Role::Owner).await;
let (version_id, _real_witness_key) =
seed_witnessed_genesis(&state, &owner.did, "gamma-delta").await;
let mut forger = affinidi_secrets_resolver::secrets::Secret::generate_ed25519(None, None);
let forger_pk = forger
.get_public_keymultibase()
.expect("forger key multibase");
forger.id = format!("did:key:{forger_pk}#{forger_pk}");
let bogus_proof = affinidi_data_integrity::DataIntegrityProof::sign(
&json!({ "versionId": &version_id }),
&forger,
affinidi_data_integrity::SignOptions::new(),
)
.await
.expect("sign bogus proof — well-formed, just from the wrong key");
let reply = call(
&state,
Via::Tsp,
&owner,
WITNESS,
json!({
"mnemonic": "gamma-delta",
"witness": {
"versionId": version_id,
"witness": format!("did:key:{forger_pk}"),
"proof": serde_json::to_value(&bogus_proof).unwrap(),
},
}),
)
.await;
assert_eq!(
code(&reply),
"webvh/witness/publish:invalidWitness",
"a proof from a non-witness key must be refused, not stored: {reply}"
);
assert!(
state
.dids_ks
.get_raw(did_hosting_common::did_ops::content_witness_key(
"gamma-delta"
))
.await
.unwrap()
.is_none(),
"a rejected proof must leave no witness-content behind",
);
}
async fn seed_names(state: &AppState, owner: &str, mnemonic: &str, names: &[(&str, bool)]) {
let mut record = seed_did(state, owner, mnemonic).await;
record.domain = "example.com".into();
record.agent_names = names
.iter()
.map(|(name, enabled)| AgentNameEntry {
name: (*name).into(),
enabled: *enabled,
created_at: 7,
})
.collect();
state
.dids_ks
.insert(did_key(mnemonic), &record)
.await
.unwrap();
}
#[tokio::test]
async fn agent_name_list_answers_the_registry_parked_names_included() {
let (state, _dir) = state().await;
let owner = member(&state, 30, Role::Owner).await;
let attacker = member(&state, 31, Role::Owner).await;
seed_names(
&state,
&owner.did,
"alpha-beta",
&[("alice", true), ("parked", false)],
)
.await;
seed_names(&state, &owner.did, "empty", &[]).await;
let reply = call(
&state,
Via::Tsp,
&owner,
NAME_LIST,
json!({ "mnemonic": "alpha-beta" }),
)
.await;
conforms(&reply);
let body = ok(&reply, NAME_LIST);
assert_eq!(body["domain"], "example.com");
assert_eq!(body["agentNames"][0]["name"], "alice");
assert_eq!(body["agentNames"][0]["createdAt"], "1970-01-01T00:00:07Z");
assert_eq!(body["agentNames"][1]["enabled"], false);
let body = ok(
&call(
&state,
Via::Tsp,
&owner,
NAME_LIST,
json!({ "mnemonic": "empty" }),
)
.await,
NAME_LIST,
);
assert_eq!(body["agentNames"], json!([]));
let reply = call(
&state,
Via::Tsp,
&owner,
NAME_LIST,
json!({ "mnemonic": "alpha-beta", "domain": "other.example" }),
)
.await;
assert_eq!(code(&reply), "did-management:unknownDomain", "{reply}");
let reply = call(
&state,
Via::Tsp,
&attacker,
NAME_LIST,
json!({ "mnemonic": "alpha-beta" }),
)
.await;
assert_eq!(
code(&reply),
"did-management/agent-name/list:notOwner",
"{reply}"
);
}
#[tokio::test]
async fn agent_name_check_reports_free_reserved_and_taken() {
let (state, _dir) = state().await;
let owner = member(&state, 32, Role::Owner).await;
state
.dids_ks
.insert_raw(
did_hosting_common::did_ops::agent_name_key("example.com", "taken"),
b"alpha-beta".to_vec(),
)
.await
.unwrap();
for (name, available, reserved) in [
("alice", true, false),
("admin", false, true),
("taken", false, false),
] {
let reply = call(
&state,
Via::Tsp,
&owner,
NAME_CHECK,
json!({ "name": name, "domain": "example.com" }),
)
.await;
conforms(&reply);
let body = ok(&reply, NAME_CHECK);
assert_eq!(body["name"], name);
assert_eq!(body["domain"], "example.com");
assert_eq!(body["available"], available, "{name}");
assert_eq!(body["reserved"], reserved, "{name}");
}
let reply = call(
&state,
Via::Tsp,
&owner,
NAME_CHECK,
json!({ "name": "alice" }),
)
.await;
assert_eq!(code(&reply), "did-management:unknownDomain", "{reply}");
}
#[tokio::test]
async fn agent_name_update_and_remove_reach_the_engine_as_the_caller() {
let (state, _dir) = state().await;
let owner = member(&state, 33, Role::Owner).await;
let attacker = member(&state, 34, Role::Owner).await;
seed_did(&state, &owner.did, "alpha-beta").await;
let reply = call(
&state,
Via::Tsp,
&owner,
NAME_UPDATE,
json!({ "mnemonic": "alpha-beta", "name": "alice", "state": "active", "didData": "not-a-valid-log" }),
)
.await;
assert_eq!(
code(&reply),
"did-management/agent-name/update:invalidDidData",
"{reply}"
);
let reply = call(
&state,
Via::Tsp,
&owner,
NAME_UPDATE,
json!({ "mnemonic": "alpha-beta", "name": "admin", "state": "active", "didData": "x" }),
)
.await;
assert_eq!(
code(&reply),
"did-management/agent-name/update:nameReserved",
"{reply}"
);
let reply = call(
&state,
Via::Tsp,
&owner,
NAME_UPDATE,
json!({ "mnemonic": "alpha-beta", "name": "alice", "state": "released", "didData": "x" }),
)
.await;
assert_eq!(code(&reply), "malformedRequest", "{reply}");
let reply = call(
&state,
Via::Tsp,
&attacker,
NAME_REMOVE,
json!({ "mnemonic": "alpha-beta", "name": "alice", "didData": "x" }),
)
.await;
assert_eq!(
code(&reply),
"did-management/agent-name/remove:notOwner",
"{reply}"
);
}
#[tokio::test]
async fn me_domains_answers_in_the_shared_domain_entry_shape() {
let (state, _dir) = state().await;
let owner = member(&state, 35, Role::Owner).await;
did_hosting_common::server::domain::create_domain(
&state.store,
&did_hosting_common::server::domain::DomainEntry {
name: "example.com".into(),
label: Some("Example".into()),
scheme: did_hosting_common::server::domain::DomainUrlScheme::Https,
status: did_hosting_common::server::domain::DomainStatus::Active,
created_at: 7,
default_domain: true,
branding: None,
witnesses: None,
watchers: None,
quota: None,
well_known_enabled: false,
disabled_at: None,
purge_at: None,
},
)
.await
.unwrap();
let uri = "https://trusttasks.org/spec/did-management/me/domains/0.1";
let reply = call(&state, Via::Tsp, &owner, uri, json!({})).await;
conforms(&reply);
let body = ok(&reply, uri);
assert_eq!(body["domains"][0]["name"], "example.com");
assert_eq!(body["domains"][0]["status"], "active");
assert_eq!(body["domains"][0]["createdAt"], "1970-01-01T00:00:07Z");
assert_eq!(
body["domains"][0]["ext"]["vnd.affinidi.webvh"]["scheme"],
"https"
);
}
const SPEC: &str = "https://trusttasks.org/spec/";
fn t(slug: &str) -> String {
format!("{SPEC}{slug}")
}
#[tokio::test]
async fn step_up_elevates_the_session_and_refresh_reads_the_new_level() {
let (state, _dir) = state().await;
let owner = member(&state, 40, Role::Owner).await;
let tokens = session_for(&state, &owner).await;
let reply = call(
&state,
Via::Didcomm,
&owner,
&t("auth/step-up/start/0.1"),
json!({ "sessionId": tokens.session_id }),
)
.await;
conforms(&reply);
let request = ok(&reply, &t("auth/step-up/start/0.1"))["approveRequest"].clone();
assert_eq!(request["type"], t("auth/step-up/approve-request/0.3"));
assert_eq!(request["issuer"], CONTROL);
assert_eq!(request["recipient"], owner.did.as_str());
assert_eq!(request["payload"]["sessionId"], tokens.session_id.as_str());
assert_eq!(request["proof"]["proofPurpose"], "authentication");
let challenge = request["payload"]["challenge"]
.as_str()
.unwrap()
.to_string();
let decision = json!({
"challenge": challenge,
"decision": "approved",
"subject": owner.did,
"sessionId": tokens.session_id,
});
let wrong = signed(
request_doc(
&t("auth/step-up/approve-response/0.5"),
&owner.did,
decision.clone(),
),
&owner.key,
)
.await;
assert_eq!(
code(&send(&state, Via::Https, &owner, wrong).await),
"proofInvalid"
);
let reply = call(
&state,
Via::Https,
&owner,
&t("auth/step-up/approve-response/0.5"),
decision.clone(),
)
.await;
conforms(&reply);
let body = ok(&reply, &t("auth/step-up/approve-response/0.5"));
assert_eq!(body["status"], "elevated");
assert_eq!(body["session"]["acr"], "aal2");
let reply = call(
&state,
Via::Https,
&owner,
&t("auth/step-up/approve-response/0.5"),
decision,
)
.await;
assert_eq!(
code(&reply),
"auth/step-up/approve-response:challengeUnknown",
"{reply}"
);
let reply = call(
&state,
Via::Tsp,
&owner,
&t("auth/refresh/0.1"),
json!({ "refreshToken": tokens.refresh_token }),
)
.await;
let body = ok(&reply, &t("auth/refresh/0.1"));
assert_eq!(body["session"]["acr"], "aal2", "{body}");
let access = body["tokens"]["accessToken"].as_str().unwrap();
let claims = state.jwt_keys.as_deref().unwrap().decode(access).unwrap();
assert_eq!(claims.acr, "aal2");
let reply = call(
&state,
Via::Tsp,
&owner,
&t("auth/step-up/start/0.1"),
json!({ "sessionId": tokens.session_id }),
)
.await;
assert_eq!(code(&reply), "auth/step-up/start:notNeeded", "{reply}");
}
#[tokio::test]
async fn step_up_start_answers_someone_elses_session_as_unknown() {
let (state, _dir) = state().await;
let owner = member(&state, 41, Role::Owner).await;
let other = member(&state, 42, Role::Owner).await;
let tokens = session_for(&state, &owner).await;
let reply = call(
&state,
Via::Tsp,
&other,
&t("auth/step-up/start/0.1"),
json!({ "sessionId": tokens.session_id }),
)
.await;
assert_eq!(code(&reply), "auth/step-up/start:sessionUnknown", "{reply}");
}
fn request_doc(type_uri: &str, issuer: &str, payload: Value) -> Value {
request(type_uri, issuer, payload)
}
#[tokio::test]
async fn server_info_answers_an_anonymous_request_signed_by_the_service() {
let (state, _dir) = state().await;
let uri = t("did-management/server/info/0.1");
let mut doc = request(&uri, "unused", json!({}));
doc.as_object_mut().unwrap().remove("issuer");
let reply = https(&state, None, doc).await;
conforms(&reply);
let body = ok(&reply, &uri);
assert_eq!(body["serviceDid"], CONTROL);
assert_eq!(reply["issuer"], CONTROL);
assert!(reply.get("recipient").is_none(), "{reply}");
assert!(reply["proof"].is_object());
}
#[tokio::test]
async fn passkey_login_start_is_open_to_an_unsigned_request() {
let (state, _dir) = state().await;
let uri = t("auth/passkey/login/start/0.2");
let mut doc = request(&uri, "unused", json!({}));
doc.as_object_mut().unwrap().remove("issuer");
let reply = https(&state, None, doc).await;
assert_eq!(
code(&reply),
"auth/passkey/login/start:noCredentials",
"{reply}"
);
}
#[tokio::test]
async fn invites_are_managed_by_invite_id_and_never_disclose_a_token() {
let (state, _dir) = state().await;
let admin = member(&state, 43, Role::Admin).await;
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/0.2"),
json!({ "subject": "did:example:invitee" }),
)
.await;
let issued = ok(&reply, &t("auth/passkey/enroll/invite/0.2"));
let token = issued["invite"]["token"].as_str().unwrap().to_string();
let listed = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/list/0.1"),
json!({}),
)
.await;
let invite_id =
ok(&listed, &t("auth/passkey/enroll/invite/list/0.1"))["invites"][0]["inviteId"]
.as_str()
.unwrap()
.to_string();
struct Created {
token: String,
invite_id: String,
}
let created = Created { token, invite_id };
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/list/0.1"),
json!({}),
)
.await;
conforms(&reply);
let text = reply.to_string();
assert!(
!text.contains(&created.token),
"a list must not carry the token"
);
let body = ok(&reply, &t("auth/passkey/enroll/invite/list/0.1"));
assert_eq!(body["invites"][0]["inviteId"], created.invite_id.as_str());
assert_eq!(body["invites"][0]["subject"], "did:example:invitee");
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/update/0.1"),
json!({ "inviteId": created.invite_id, "role": "admin" }),
)
.await;
conforms(&reply);
assert!(!reply.to_string().contains(&created.token));
assert_eq!(
ok(&reply, &t("auth/passkey/enroll/invite/update/0.1"))["invite"]["role"],
"admin"
);
let later = (chrono::Utc::now() + chrono::Duration::hours(2))
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/update/0.1"),
json!({ "inviteId": created.invite_id, "role": "owner", "expiresAt": later }),
)
.await;
conforms(&reply);
assert_eq!(
ok(&reply, &t("auth/passkey/enroll/invite/update/0.1"))["invite"]["role"],
"owner"
);
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/update/0.1"),
json!({ "inviteId": created.invite_id }),
)
.await;
assert_eq!(code(&reply), "malformedRequest", "{reply}");
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/update/0.1"),
json!({ "inviteId": created.invite_id, "role": "admin", "expiresAt": later, "extendBy": 60 }),
)
.await;
assert_eq!(code(&reply), "malformedRequest", "{reply}");
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/list/0.1"),
json!({}),
)
.await;
let body = ok(&reply, &t("auth/passkey/enroll/invite/list/0.1"));
assert_eq!(body["invites"][0]["role"], "owner", "{reply}");
assert_eq!(body["invites"][0]["expiresAt"], later, "{reply}");
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/update/0.1"),
json!({ "inviteId": created.invite_id, "expiresAt": "2099-01-01T00:00:00Z" }),
)
.await;
assert_eq!(code(&reply), "malformedRequest", "{reply}");
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/revoke/0.1"),
json!({ "inviteId": created.invite_id }),
)
.await;
conforms(&reply);
let reply = call(
&state,
Via::Tsp,
&admin,
&t("auth/passkey/enroll/invite/revoke/0.1"),
json!({ "inviteId": created.invite_id }),
)
.await;
assert_eq!(
code(&reply),
"auth/passkey/enroll/invite/revoke:notFound",
"{reply}"
);
let owner = member(&state, 44, Role::Owner).await;
let reply = call(
&state,
Via::Tsp,
&owner,
&t("auth/passkey/enroll/invite/list/0.1"),
json!({}),
)
.await;
assert_eq!(
code(&reply),
"auth/passkey/enroll/invite/list:notAdministrator",
"{reply}"
);
}
#[tokio::test]
async fn set_state_suspends_a_slot_and_queues_the_state_to_edges() {
let (state, _dir) = state().await;
let owner = member(&state, 45, Role::Owner).await;
seed_did(&state, &owner.did, "alpha").await;
let uri = t("did-management/did/set-state/0.1");
let reply = call(
&state,
Via::Tsp,
&owner,
&uri,
json!({ "mnemonic": "alpha", "state": "suspended" }),
)
.await;
conforms(&reply);
assert_eq!(ok(&reply, &uri)["record"]["disabled"], true);
let record: DidRecord = state.dids_ks.get(did_key("alpha")).await.unwrap().unwrap();
assert!(record.disabled);
let body = crate::server_push::sync_update_body(&record, "{}".into(), None).unwrap();
assert_eq!(body["disabled"], true);
}
#[tokio::test]
async fn rollback_and_log_follow_the_slots_history() {
let (state, _dir) = state().await;
let owner = member(&state, 46, Role::Owner).await;
seed_published(&state, &owner.did, "alpha").await;
let log = t("did-management/did/log/0.1");
let reply = call(
&state,
Via::Tsp,
&owner,
&log,
json!({ "mnemonic": "alpha", "raw": true }),
)
.await;
conforms(&reply);
let body = ok(&reply, &log);
assert_eq!(body["entries"].as_array().unwrap().len(), 2);
assert!(body["logContent"].as_str().unwrap().contains("2-b"));
let rollback = t("did-management/did/rollback/0.1");
let reply = call(
&state,
Via::Tsp,
&owner,
&rollback,
json!({ "mnemonic": "alpha", "targetVersion": 5 }),
)
.await;
assert_eq!(
code(&reply),
"did-management/did/rollback:invalidTargetVersion",
"{reply}"
);
let reply = call(
&state,
Via::Tsp,
&owner,
&rollback,
json!({ "mnemonic": "alpha", "targetVersion": 1 }),
)
.await;
conforms(&reply);
let body = ok(&reply, &rollback);
assert_eq!(body["removedVersions"], 1);
assert_eq!(body["record"]["versionCount"], 1);
let other = member(&state, 47, Role::Owner).await;
let reply = call(
&state,
Via::Tsp,
&other,
&log,
json!({ "mnemonic": "alpha" }),
)
.await;
assert_eq!(code(&reply), "did-management/did/log:notFound");
let reply = call(
&state,
Via::Tsp,
&other,
&log,
json!({ "mnemonic": "nothing" }),
)
.await;
assert_eq!(code(&reply), "did-management/did/log:notFound");
}
#[tokio::test]
async fn agent_name_resolve_does_not_disclose_other_owners_dids() {
let (state, _dir) = state().await;
let owner = member(&state, 48, Role::Owner).await;
let other = member(&state, 49, Role::Owner).await;
let mut record = seed_did(&state, &owner.did, "alpha").await;
record.agent_names = vec![AgentNameEntry {
name: "alice".into(),
enabled: true,
created_at: 1,
}];
state
.dids_ks
.insert(did_key("alpha"), &record)
.await
.unwrap();
let did = record.did_id.clone().unwrap();
let uri = t("did-management/agent-name/resolve/0.1");
let body = ok(
&call(&state, Via::Tsp, &owner, &uri, json!({ "dids": [did] })).await,
&uri,
);
assert_eq!(body["entries"][0]["names"], json!(["alice"]));
let body = ok(
&call(&state, Via::Tsp, &other, &uri, json!({ "dids": [did] })).await,
&uri,
);
assert_eq!(body["entries"], json!([]));
}
#[tokio::test]
async fn domains_are_administered_over_trust_tasks() {
let (state, _dir) = state().await;
let admin = member(&state, 50, Role::Admin).await;
let create = t("did-management/domain/create/0.1");
for name in ["a.example.com", "b.example.com"] {
let reply = call(
&state,
Via::Tsp,
&admin,
&create,
json!({ "name": name, "setAsDefault": name == "a.example.com", "label": "A" }),
)
.await;
conforms(&reply);
}
let reply = call(
&state,
Via::Tsp,
&admin,
&create,
json!({ "name": "a.example.com", "setAsDefault": false }),
)
.await;
assert_eq!(
code(&reply),
"did-management/domain/create:domainExists",
"{reply}"
);
let list = t("did-management/domain/list/0.1");
let reply = call(&state, Via::Tsp, &admin, &list, json!({})).await;
conforms(&reply);
let body = ok(&reply, &list);
assert_eq!(body["default"], "a.example.com");
assert_eq!(body["domains"].as_array().unwrap().len(), 2);
let set_state = t("did-management/domain/set-state/0.1");
let reply = call(
&state,
Via::Tsp,
&admin,
&set_state,
json!({ "name": "a.example.com", "state": "disabled" }),
)
.await;
assert_eq!(
code(&reply),
"did-management/domain/set-state:isDefault",
"{reply}"
);
let reply = call(
&state,
Via::Tsp,
&admin,
&set_state,
json!({ "name": "b.example.com", "state": "disabled" }),
)
.await;
conforms(&reply);
assert_eq!(ok(&reply, &set_state)["entry"]["status"], "disabled");
let set_default = t("did-management/domain/set-default/0.1");
let reply = call(
&state,
Via::Tsp,
&admin,
&set_default,
json!({ "name": "b.example.com" }),
)
.await;
assert_eq!(
code(&reply),
"did-management/domain/set-default:domainDisabled",
"{reply}"
);
let purge = t("did-management/domain/purge/0.1");
let payload = json!({ "name": "b.example.com", "purgeServers": false });
let reply = call(&state, Via::Tsp, &admin, &purge, payload.clone()).await;
assert_eq!(
code(&reply),
"permissionDenied",
"a base session cannot purge: {reply}"
);
let bearer = crate::auth::AuthClaims {
did: admin.did.clone(),
role: Role::Admin,
session_id: "s".into(),
session_pubkey_b58btc: None,
amr: vec!["did".into()],
acr: "aal2".into(),
};
let doc = signed(request(&purge, &admin.did, payload), &admin.key).await;
let reply = https(&state, Some(bearer), doc).await;
conforms(&reply);
assert_eq!(ok(&reply, &purge)["name"], "b.example.com");
let owner = member(&state, 51, Role::Owner).await;
let reply = call(&state, Via::Tsp, &owner, &list, json!({})).await;
assert_eq!(code(&reply), "permissionDenied");
}
#[tokio::test]
async fn the_fleet_is_administered_over_trust_tasks() {
let (state, _dir) = state().await;
let admin = member(&state, 52, Role::Admin).await;
did_hosting_common::server::domain::create_domain(
&state.store,
&did_hosting_common::server::domain::DomainEntry {
name: "example.com".into(),
label: None,
scheme: did_hosting_common::server::domain::DomainUrlScheme::Https,
status: did_hosting_common::server::domain::DomainStatus::Active,
created_at: 1,
default_domain: false,
branding: None,
witnesses: None,
watchers: None,
quota: None,
well_known_enabled: false,
disabled_at: None,
purge_at: None,
},
)
.await
.unwrap();
let register = t("did-management/registry/admin-register/0.1");
let payload = json!({
"instanceId": "edge-1",
"did": "did:example:edge-1",
"publicUrl": "https://edge-1.example.com",
"servedDomains": ["example.com"],
});
let reply = call(&state, Via::Tsp, &admin, ®ister, payload.clone()).await;
conforms(&reply);
let reply = call(&state, Via::Tsp, &admin, ®ister, payload).await;
assert_eq!(
code(&reply),
"did-management/registry/admin-register:instanceExists"
);
for slug in ["registry/get/0.1", "registry/check/0.1"] {
let uri = t(&format!("did-management/{slug}"));
let reply = call(
&state,
Via::Tsp,
&admin,
&uri,
json!({ "instanceId": "edge-1" }),
)
.await;
conforms(&reply);
assert_eq!(ok(&reply, &uri)["instance"]["did"], "did:example:edge-1");
}
let list = t("did-management/registry/list/0.1");
let reply = call(
&state,
Via::Tsp,
&admin,
&list,
json!({ "serviceType": "server" }),
)
.await;
conforms(&reply);
assert_eq!(ok(&reply, &list)["instances"].as_array().unwrap().len(), 1);
let purge = t("did-management/registry/purge-domain/0.1");
let reply = call(
&state,
Via::Tsp,
&admin,
&purge,
json!({ "instanceId": "edge-1", "domain": "example.com" }),
)
.await;
assert_eq!(
code(&reply),
"did-management/registry/purge-domain:stillAssigned",
"{reply}"
);
let deregister = t("did-management/registry/deregister/0.1");
let reply = call(
&state,
Via::Tsp,
&admin,
&deregister,
json!({ "instanceId": "edge-1" }),
)
.await;
conforms(&reply);
let reply = call(
&state,
Via::Tsp,
&admin,
&deregister,
json!({ "instanceId": "edge-1" }),
)
.await;
assert_eq!(code(&reply), "did-management/registry/deregister:notFound");
}
#[tokio::test]
async fn the_service_describes_itself_to_administrators_only() {
let (state, _dir) = state().await;
let admin = member(&state, 53, Role::Admin).await;
let owner = member(&state, 54, Role::Owner).await;
for slug in [
"server/config/0.1",
"server/metrics/0.1",
"identity/list/0.1",
"stats/get/0.1",
] {
let uri = t(&format!("did-management/{slug}"));
let reply = call(&state, Via::Tsp, &admin, &uri, json!({})).await;
conforms(&reply);
let reply = call(&state, Via::Tsp, &owner, &uri, json!({})).await;
assert_eq!(code(&reply), "permissionDenied", "{slug}: {reply}");
}
let current = state.identity.as_ref().unwrap().generations()[0].id;
let uri = t("did-management/identity/retire/0.1");
let reply = call(
&state,
Via::Tsp,
&admin,
&uri,
json!({ "generationId": current }),
)
.await;
assert_eq!(
code(&reply),
"did-management/identity/retire:current",
"{reply}"
);
let reply = call(
&state,
Via::Tsp,
&admin,
&uri,
json!({ "generationId": current + 100 }),
)
.await;
assert_eq!(
code(&reply),
"did-management/identity/retire:notFound",
"{reply}"
);
seed_did(&state, &owner.did, "mine").await;
let uri = t("did-management/stats/get/0.1");
let reply = call(
&state,
Via::Tsp,
&owner,
&uri,
json!({ "mnemonic": "mine" }),
)
.await;
conforms(&reply);
let reply = call(
&state,
Via::Tsp,
&admin,
&uri,
json!({ "mnemonic": "ghost" }),
)
.await;
assert_eq!(code(&reply), "did-management/stats/get:notFound");
let uri = t("did-management/stats/timeseries/0.1");
let reply = call(
&state,
Via::Tsp,
&owner,
&uri,
json!({ "range": "lastDay", "mnemonic": "mine" }),
)
.await;
conforms(&reply);
assert_eq!(ok(&reply, &uri)["bucketSeconds"], 900);
let reply = call(
&state,
Via::Tsp,
&owner,
&uri,
json!({ "range": "lastDay" }),
)
.await;
assert_eq!(code(&reply), "permissionDenied");
}
#[tokio::test]
async fn metrics_reader_reads_counters_only_and_is_denied_elsewhere() {
let (state, _dir) = state().await;
let reader = member(&state, 71, Role::MetricsReader).await;
let metrics = t("did-management/server/metrics/0.1");
let reply = call(&state, Via::Tsp, &reader, &metrics, json!({})).await;
conforms(&reply);
let snapshot = ok(&reply, &metrics)["snapshot"].clone();
assert!(snapshot["counters"].is_array(), "counters present: {reply}");
assert_eq!(
snapshot["gauges"].as_array().map(Vec::len),
Some(0),
"a MetricsReader must not see replication gauges: {reply}"
);
let reserve = call(
&state,
Via::Tsp,
&reader,
&t("did-management/did/check-name/0.1"),
json!({ "reserve": true }),
)
.await;
assert_eq!(code(&reserve), "permissionDenied", "check-name: {reserve}");
for slug in ["server/config/0.1", "identity/list/0.1"] {
let uri = t(&format!("did-management/{slug}"));
let reply = call(&state, Via::Tsp, &reader, &uri, json!({})).await;
assert_eq!(code(&reply), "permissionDenied", "{slug}: {reply}");
}
}
async fn seed_named(state: &AppState, mnemonic: &str, did_id: &str, names: &[(&str, bool)]) {
let mut record = seed_did(state, "did:example:operator", mnemonic).await;
record.did_id = Some(did_id.into());
record.agent_names = names
.iter()
.map(|(n, enabled)| AgentNameEntry {
name: (*n).into(),
enabled: *enabled,
created_at: 0,
})
.collect();
state
.dids_ks
.insert(did_key(mnemonic), &record)
.await
.unwrap();
}
async fn anonymous_server_info(state: &AppState) -> Value {
let uri = t("did-management/server/info/0.1");
let mut doc = request(&uri, "unused", json!({}));
doc.as_object_mut().unwrap().remove("issuer");
ok(&https(state, None, doc).await, &uri)
}
fn names_of(info: &Value) -> Vec<String> {
info["serviceNames"]
.as_array()
.expect("serviceNames is always present")
.iter()
.map(|v| v.as_str().expect("a name is a string").to_string())
.collect()
}
#[tokio::test]
async fn server_info_says_whether_agent_names_are_served() {
let (mut state, _dir) = state().await;
assert_eq!(anonymous_server_info(&state).await["agentNames"], true);
let mut config = (*state.config).clone();
config.features.agent_names = false;
state.config = std::sync::Arc::new(config);
assert_eq!(anonymous_server_info(&state).await["agentNames"], false);
}
#[tokio::test]
async fn server_info_names_the_services_own_served_agent_names() {
let (state, _dir) = state().await;
assert!(names_of(&anonymous_server_info(&state).await).is_empty());
seed_named(&state, ".well-known", CONTROL, &[("", true)]).await;
assert_eq!(names_of(&anonymous_server_info(&state).await), vec![""]);
seed_named(
&state,
".well-known",
CONTROL,
&[("", false), ("live", true)],
)
.await;
assert_eq!(names_of(&anonymous_server_info(&state).await), vec!["live"]);
}
#[tokio::test]
async fn server_info_does_not_advertise_names_it_would_not_serve() {
let (mut state, _dir) = state().await;
seed_named(
&state,
".well-known",
"did:webvh:other:someone-else.example",
&[("", true)],
)
.await;
assert!(names_of(&anonymous_server_info(&state).await).is_empty());
seed_named(&state, ".well-known", CONTROL, &[("", true)]).await;
let mut config = (*state.config).clone();
config.features.agent_names = false;
state.config = std::sync::Arc::new(config);
assert!(names_of(&anonymous_server_info(&state).await).is_empty());
}
#[tokio::test]
async fn agent_name_resolve_answers_only_served_names_of_hosted_dids() {
let (state, _dir) = state().await;
let admin = member(&state, 60, Role::Admin).await;
let root = "did:webvh:abc:control.example.com";
let bob = "did:webvh:abc:control.example.com:bob";
let quiet = "did:webvh:abc:control.example.com:quiet";
seed_named(&state, ".well-known", root, &[("", true)]).await;
seed_named(&state, "bob", bob, &[("parked", false), ("live", true)]).await;
seed_named(&state, "quiet", quiet, &[]).await;
let uri = t("did-management/agent-name/resolve/0.1");
let body = ok(
&call(
&state,
Via::Tsp,
&admin,
&uri,
json!({ "dids": [
root,
bob,
quiet,
"did:webvh:zzz:someone-else.example",
"did:web:elsewhere.example",
] }),
)
.await,
&uri,
);
let mut entries = body["entries"].as_array().unwrap().clone();
entries.sort_by_key(|e| e["did"].as_str().unwrap().to_string());
assert_eq!(
entries,
vec![
json!({ "did": root, "names": [""] }),
json!({ "did": bob, "names": ["live"] }),
]
);
}
#[tokio::test]
async fn advertised_services_are_reported_and_unknown_is_omitted() {
use crate::registry::{self, ServiceInstance, ServiceStatus, ServiceType};
let (state, _dir) = state().await;
assert!(state.did_resolver.is_none());
let admin = member(&state, 61, Role::Admin).await;
let instance = ServiceInstance {
instance_id: "srv-1".into(),
service_type: ServiceType::Server,
label: Some("edge".into()),
url: "http://edge.example".into(),
status: ServiceStatus::Active,
last_health_check: None,
registered_at: 1_700_000_000,
metadata: json!({ "did": "did:webvh:Q1:edge.example" }),
enabled_methods: vec!["webvh".into()],
served_domains: vec![],
protocol_version: "1.0".into(),
advertised_services: Some(vec!["WebVHHosting".into(), "TSPTransport".into()]),
services_checked_at: Some(1_700_000_000),
trust_task_capable: false,
sync_batch_capable: false,
last_inbound_transport: None,
last_inbound_at: None,
last_outbound_transport: None,
last_outbound_at: None,
last_ack_at: None,
last_reconcile_at: None,
};
registry::register_instance(&state.registry_ks, &instance)
.await
.unwrap();
let uri = t("did-management/registry/list/0.1");
let reply = call(&state, Via::Tsp, &admin, &uri, json!({})).await;
conforms(&reply);
let body = ok(&reply, &uri);
assert_eq!(
body["instances"][0]["advertisedServices"],
json!(["WebVHHosting", "TSPTransport"]),
"{body}"
);
let uri = t("did-management/server/config/0.1");
let reply = call(&state, Via::Tsp, &admin, &uri, json!({})).await;
conforms(&reply);
let body = ok(&reply, &uri);
assert!(body.get("advertisedServices").is_none(), "{body}");
assert!(body["transports"].get("tsp").is_some(), "{body}");
assert!(body["transports"].get("didcomm").is_some(), "{body}");
}
#[test]
fn existing_rows_keep_their_proof_rules() {
const BEFORE: &[(&str, ProofRule)] = &[
(
"https://trusttasks.org/spec/did-management/did/check-name/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/did/register/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/did/info/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/did/list/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/did/delete/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/did/change-owner/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/did/set-state/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/did/rollback/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/did/log/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/webvh/witness/publish/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/agent-name/update/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/agent-name/remove/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/agent-name/list/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/agent-name/check/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/agent-name/resolve/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/me/domains/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/domain/list/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/domain/create/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/domain/update/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/domain/set-state/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/domain/set-default/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/domain/purge/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/domain/assign/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/domain/unassign/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/registry/list/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/registry/get/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/registry/check/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/registry/admin-register/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/registry/deregister/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/registry/purge-domain/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/server/info/0.1",
ProofRule::Optional,
),
(
"https://trusttasks.org/spec/did-management/server/config/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/server/metrics/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/stats/get/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/stats/timeseries/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/identity/list/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/did-management/identity/retire/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/step-up/start/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/step-up/approve-response/0.5",
ProofRule::AssertionMethod,
),
(
"https://trusttasks.org/spec/auth/revoke-session/0.2",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/login/start/0.2",
ProofRule::Optional,
),
(
"https://trusttasks.org/spec/auth/passkey/login/finish/0.2",
ProofRule::SessionKey,
),
(
"https://trusttasks.org/spec/auth/passkey/enroll/invite/0.2",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/enroll/redeem/start/0.1",
ProofRule::Optional,
),
(
"https://trusttasks.org/spec/auth/passkey/enroll/redeem/finish/0.1",
ProofRule::Optional,
),
(
"https://trusttasks.org/spec/auth/passkey/enroll/start/0.2",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/enroll/finish/0.2",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/enroll/invite/list/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/enroll/invite/update/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/enroll/invite/revoke/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/list/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/admin-list/0.1",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/revoke/start/0.2",
ProofRule::Authentication,
),
(
"https://trusttasks.org/spec/auth/passkey/revoke/finish/0.2",
ProofRule::Authentication,
),
];
let now: Vec<_> = did_signed_rows().copied().collect();
assert_eq!(now, BEFORE);
}
#[test]
fn bound_key_rows_are_wallet_sign_in_and_nothing_else() {
use crate::oob::types::*;
let bound: Vec<_> = TASKS
.iter()
.filter_map(|(uri, rule)| match rule {
ProofRule::BoundKey(purposes) => Some((*uri, *purposes)),
_ => None,
})
.collect();
let purposes: &[&str] = &["authentication", "assertionMethod"];
assert_eq!(
bound,
[
TYPE_REQUEST,
TYPE_CLAIM,
TYPE_PROVE,
TYPE_PROVE_V0_2,
TYPE_RESPOND,
TYPE_RESPOND_V0_2,
TYPE_REDEEM,
TYPE_CANCEL
]
.map(|uri| (uri, purposes))
);
for carried in [TYPE_IDENTIFY, TYPE_GRANT, TYPE_GRANT_V0_2] {
assert_eq!(super::proof_rule(carried), None, "{carried} is not a row");
}
for (uri, rule) in TASKS {
let bound = matches!(rule, ProofRule::BoundKey(_));
assert_eq!(rule.admits_bearer(), !bound, "{uri}");
assert_eq!(rule.uses_shared_replay_cache(), !bound, "{uri}");
assert_eq!(
crate::messaging::session_key_may_sign(uri),
*rule == ProofRule::Authentication,
"{uri}"
);
}
}
async fn redeem_doc(starter: &Caller, request_id: &str, purpose: &str) -> Value {
let doc = request(
crate::oob::types::TYPE_REDEEM,
&starter.did,
json!({ "requestId": request_id }),
);
match purpose {
"authentication" => signed(doc, &starter.key).await,
"assertionMethod" => signed_as_assertion(doc, &starter.key).await,
other => {
use trust_tasks_proof::affinidi::{CryptoSuite, SignOptions, sign_trust_task};
sign_trust_task(
&doc,
&starter.key,
SignOptions::new()
.with_proof_purpose(other)
.with_cryptosuite(CryptoSuite::EddsaJcs2022),
)
.await
.expect("sign")
}
}
}
const NO_SUCH_REQUEST: &str = "AAAAAAAAAAAAAAAAAAAAAA";
#[tokio::test]
async fn bound_key_rows_are_served_over_https_only() {
let (state, _dir) = state().await;
let starter = stranger(60);
for via in VIAS {
let doc = redeem_doc(&starter, NO_SUCH_REQUEST, "authentication").await;
let reply = send(&state, via, &starter, doc).await;
match via {
Via::Https => assert_eq!(code(&reply), "auth/oob:requestNotFound", "{reply}"),
_ => {
assert_eq!(code(&reply), "permissionDenied", "{via:?}: {reply}");
assert!(
reply["payload"]["message"]
.as_str()
.is_some_and(|m| m.contains("HTTPS only")),
"{via:?}: {reply}"
);
}
}
}
}
#[tokio::test]
async fn an_unserved_prove_version_is_unsupported_so_a_wallet_falls_back() {
let (state, _dir) = state().await;
let approver = stranger(64);
let prove = |type_uri: &'static str| {
let approver = &approver;
async move {
let mut doc = request(type_uri, &approver.did, json!({ "identify": {} }));
doc["parentThreadId"] = json!(NO_SUCH_REQUEST);
signed(doc, &approver.key).await
}
};
for served in [
crate::oob::types::TYPE_PROVE,
crate::oob::types::TYPE_PROVE_V0_2,
] {
let reply = https(&state, None, prove(served).await).await;
assert_ne!(code(&reply), "unsupportedType", "{served}: {reply}");
}
let reply = https(
&state,
None,
prove("https://trusttasks.org/spec/auth/oob/prove/0.99").await,
)
.await;
assert!(code(&reply).ends_with("unsupportedType"), "{reply}");
}
#[tokio::test]
async fn bound_key_rows_ignore_a_bearer_session() {
let (state, _dir) = state().await;
let admin = member(&state, 61, Role::Admin).await;
let bearer = crate::auth::AuthClaims {
did: admin.did.clone(),
role: Role::Admin,
session_id: "s".into(),
session_pubkey_b58btc: None,
amr: vec!["did".into()],
acr: "aal2".into(),
};
let starter = stranger(62);
let doc = redeem_doc(&starter, NO_SUCH_REQUEST, "authentication").await;
let reply = https(&state, Some(bearer), doc).await;
assert_eq!(code(&reply), "auth/oob:requestNotFound", "{reply}");
}
#[tokio::test]
async fn bound_key_rows_need_a_proof_by_the_issuer_key_for_a_listed_purpose() {
let (state, _dir) = state().await;
let starter = stranger(63);
for purpose in ["authentication", "assertionMethod"] {
let doc = redeem_doc(&starter, NO_SUCH_REQUEST, purpose).await;
let reply = https(&state, None, doc).await;
assert_eq!(
code(&reply),
"auth/oob:requestNotFound",
"{purpose}: {reply}"
);
}
let doc = redeem_doc(&starter, NO_SUCH_REQUEST, "capabilityInvocation").await;
assert_eq!(
code(&https(&state, None, doc).await),
"auth/oob:notAuthorized"
);
let doc = request(
crate::oob::types::TYPE_REDEEM,
&starter.did,
json!({ "requestId": NO_SUCH_REQUEST }),
);
assert_eq!(
code(&https(&state, None, doc).await),
"auth/oob:notAuthorized"
);
let other = stranger(64);
let mut doc = redeem_doc(&other, NO_SUCH_REQUEST, "authentication").await;
doc["issuer"] = json!(starter.did);
assert_eq!(
code(&https(&state, None, doc).await),
"auth/oob:notAuthorized"
);
let mut doc = redeem_doc(&starter, NO_SUCH_REQUEST, "authentication").await;
doc["payload"]["requestId"] = json!("BBBBBBBBBBBBBBBBBBBBBB");
assert_eq!(
code(&https(&state, None, doc).await),
"auth/oob:notAuthorized"
);
let admin = member(&state, 65, Role::Admin).await;
let mut doc = redeem_doc(&admin, NO_SUCH_REQUEST, "authentication").await;
doc["issuer"] = json!("did:webvh:abc:control.test:alice");
assert_eq!(
code(&https(&state, None, doc).await),
"auth/oob:keyUnsupported"
);
}
#[tokio::test]
async fn bound_key_rows_do_not_touch_the_shared_replay_cache() {
let (state, _dir) = state().await;
let starter = stranger(66);
let doc = redeem_doc(&starter, NO_SUCH_REQUEST, "authentication").await;
let id = doc["id"].as_str().unwrap().to_string();
let before = state.replay_cache.len();
let reply = https(&state, None, doc).await;
assert_eq!(code(&reply), "auth/oob:requestNotFound", "{reply}");
assert_eq!(state.replay_cache.len(), before);
assert!(state.replay_cache.check(&starter.did, &id).is_ok());
}
#[tokio::test]
async fn bound_key_admission_runs_before_the_proof_check() {
use crate::oob::http::OTHER_MAX_PER_MINUTE;
let (state, _dir) = state().await;
let starter = stranger(67);
for n in 0..OTHER_MAX_PER_MINUTE {
let mut doc = redeem_doc(&starter, NO_SUCH_REQUEST, "authentication").await;
doc["proof"]["proofValue"] = json!("zBad");
let reply = https(&state, None, doc).await;
assert_eq!(code(&reply), "auth/oob:notAuthorized", "{n}: {reply}");
}
let mut doc = redeem_doc(&starter, NO_SUCH_REQUEST, "authentication").await;
doc["proof"]["proofValue"] = json!("zBad");
assert_eq!(
code(&https(&state, None, doc).await),
"auth/oob:rateLimited"
);
}
#[tokio::test]
async fn bound_key_rows_refuse_an_oversized_document_before_its_proof() {
use crate::oob::http::MAX_OOB_DOCUMENT_BYTES;
let (state, _dir) = state().await;
let starter = stranger(68);
let mut doc = request(
crate::oob::types::TYPE_REDEEM,
&starter.did,
json!({ "requestId": NO_SUCH_REQUEST }),
);
doc["pad"] = json!("x".repeat(MAX_OOB_DOCUMENT_BYTES));
let doc = signed(doc, &starter.key).await;
let reply = https(&state, None, doc).await;
assert_eq!(code(&reply), "malformedRequest", "{reply}");
assert_eq!(reply["payload"]["message"], "document too large", "{reply}");
}