use metrics::counter;
use vta_sdk::trust_tasks;
#[derive(Debug, Clone, Copy)]
pub struct RestException {
pub method: &'static str,
pub path: &'static str,
pub protocol: &'static str,
pub twin: Option<&'static str>,
pub reason: &'static str,
}
const PASSKEY_VM_REASON: &str = "passkey enrolment is a WebAuthn ceremony driven from a browser \
(the VTA auth portal, examples/vta-auth-demo) that holds only the bearer token \
passkey-login issued, and no DID key with which to sign a Trust Task";
const REST_EXCEPTIONS: &[RestException] = &[
RestException {
method: "POST",
path: "/did/verification-methods/passkey/challenge",
protocol: "WebAuthn",
twin: Some(trust_tasks::TASK_PASSKEY_VMS_ENROLL_CHALLENGE_0_1),
reason: PASSKEY_VM_REASON,
},
RestException {
method: "POST",
path: "/did/verification-methods/passkey",
protocol: "WebAuthn",
twin: Some(trust_tasks::TASK_PASSKEY_VMS_ENROLL_SUBMIT_0_1),
reason: PASSKEY_VM_REASON,
},
RestException {
method: "GET",
path: "/did/verification-methods/passkey",
protocol: "WebAuthn",
twin: Some(trust_tasks::TASK_PASSKEY_VMS_LIST_0_1),
reason: PASSKEY_VM_REASON,
},
RestException {
method: "DELETE",
path: "/did/verification-methods/passkey/{fragment}",
protocol: "WebAuthn",
twin: Some(trust_tasks::TASK_PASSKEY_VMS_REVOKE_0_1),
reason: PASSKEY_VM_REASON,
},
RestException {
method: "POST",
path: "/bootstrap/request",
protocol: "pre-identity bootstrap",
twin: None,
reason: "the TEE's first boot, before any identity exists yet for it to sign a Trust \
Task with",
},
RestException {
method: "GET",
path: "/backup/blob/{bundle_id}",
protocol: "bulk byte transfer",
twin: None,
reason: "a one-shot bearer-token-gated blob fetch; the control step (authorizing the \
backup/restore) is itself a Trust Task, this is just the bytes",
},
RestException {
method: "POST",
path: "/backup/blob/{bundle_id}",
protocol: "bulk byte transfer",
twin: None,
reason: "a one-shot bearer-token-gated blob upload; the control step (authorizing the \
backup/restore) is itself a Trust Task, this is just the bytes",
},
RestException {
method: "GET",
path: "/openapi.json",
protocol: "tooling/discovery",
twin: None,
reason: "describes the API shape, not a secret; unauthenticated by design so black-box \
conformance/fuzz tooling can fetch it before it holds a token",
},
];
pub fn rest_exceptions_table() -> &'static [RestException] {
REST_EXCEPTIONS
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SupersededTask {
pub uri: &'static str,
pub successor: &'static str,
pub reason: &'static str,
}
#[allow(deprecated)] const SUPERSEDED_TASKS: &[SupersededTask] = &[
SupersededTask {
uri: trust_tasks::TASK_DID_TEMPLATES_LIST_2_0,
successor: trust_tasks::TASK_DID_TEMPLATES_LIST_3_0,
reason: "3.0 returns records that may carry a `keys` block; 2.0's schema cannot \
express one, so a post-quantum template is unreadable through it",
},
SupersededTask {
uri: trust_tasks::TASK_DID_TEMPLATES_CREATE_2_0,
successor: trust_tasks::TASK_DID_TEMPLATES_CREATE_3_0,
reason: "3.0 accepts a template declaring `schemaVersion` 2 and a `keys` block, \
which names each key slot's algorithms",
},
SupersededTask {
uri: trust_tasks::TASK_DID_TEMPLATES_GET_2_0,
successor: trust_tasks::TASK_DID_TEMPLATES_GET_3_0,
reason: "3.0 returns a record that may carry a `keys` block; 2.0's schema cannot \
express one, so a post-quantum template is unreadable through it",
},
SupersededTask {
uri: trust_tasks::TASK_DID_TEMPLATES_UPDATE_2_0,
successor: trust_tasks::TASK_DID_TEMPLATES_UPDATE_3_0,
reason: "3.0 accepts a template declaring `schemaVersion` 2 and a `keys` block, \
which names each key slot's algorithms",
},
SupersededTask {
uri: trust_tasks::TASK_CONTEXTS_UPDATE_DID_1_0,
successor: trust_tasks::TASK_CONTEXTS_UPDATE_DID_1_1,
reason: "1.1 accepts `did: null`, which clears the context's DID, and requires a \
string `did` to be a DID; 1.0 can only replace one",
},
SupersededTask {
uri: trust_tasks::TASK_AUTH_STEP_UP_APPROVE_RESPONSE_0_1,
successor: trust_tasks::TASK_AUTH_STEP_UP_APPROVE_RESPONSE_0_2,
reason: "0.2 spells the evidence enum `didSigned` in camelCase; the payload is \
signed, so the two versions have separate typed handlers rather than \
an edge transform",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_REGISTER_0_1,
successor: trust_tasks::TASK_DEVICE_REGISTER_0_2,
reason: "0.2 spells the enum values in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_HEARTBEAT_0_1,
successor: trust_tasks::TASK_DEVICE_HEARTBEAT_0_2,
reason: "0.2 spells the enum values in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_LIST_0_1,
successor: trust_tasks::TASK_DEVICE_LIST_0_2,
reason: "0.2 spells the enum values in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_SET_WAKE_0_1,
successor: trust_tasks::TASK_DEVICE_SET_WAKE_0_2,
reason: "no enum values changed; the bump is canonical-version alignment, so \
the whole device slice sits on one version",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_WIPE_0_1,
successor: trust_tasks::TASK_DEVICE_WIPE_0_2,
reason: "0.2 spells the `scope` enum value `cacheAndKeys` in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_LIST_0_1,
successor: trust_tasks::TASK_VAULT_LIST_0_2,
reason: "0.2 spells secretKind and the related enums in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_LIST_0_2,
successor: trust_tasks::TASK_VAULT_LIST_0_3,
reason: "0.3 replaces AttachmentRef's bare-hex `sha256` with a multibase \
`digestMultibase`, which names its own hash algorithm",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_GET_0_1,
successor: trust_tasks::TASK_VAULT_GET_0_2,
reason: "0.2 spells the response enums in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_GET_0_2,
successor: trust_tasks::TASK_VAULT_GET_0_3,
reason: "0.3 replaces AttachmentRef's bare-hex `sha256` with a multibase \
`digestMultibase`, which names its own hash algorithm",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_UPSERT_0_1,
successor: trust_tasks::TASK_VAULT_UPSERT_0_2,
reason: "0.2 spells the secretKind / sealed-envelope / target enums in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_UPSERT_0_2,
successor: trust_tasks::TASK_VAULT_UPSERT_0_3,
reason: "0.3 replaces AttachmentRef's bare-hex `sha256` with a multibase \
`digestMultibase`, which names its own hash algorithm",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_RELEASE_0_1,
successor: trust_tasks::TASK_VAULT_RELEASE_0_2,
reason: "0.2 spells the secretKind / sealed-envelope / step-up-proof enums in \
camelCase, inside the sealed cleartext as well as around it",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_PROXY_LOGIN_0_1,
successor: trust_tasks::TASK_VAULT_PROXY_LOGIN_0_2,
reason: "0.2 spells the site-target / step-up-proof enums in camelCase, inside \
the sealed cleartext as well as around it",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_SIGN_TRUST_TASK_0_1,
successor: trust_tasks::TASK_VAULT_SIGN_TRUST_TASK_0_2,
reason: "0.2 spells the step-up-proof enums in camelCase",
},
];
pub fn superseded_tasks_table() -> &'static [SupersededTask] {
SUPERSEDED_TASKS
}
pub fn superseded_task(type_uri: &str) -> Option<&'static SupersededTask> {
SUPERSEDED_TASKS.iter().find(|t| t.uri == type_uri)
}
pub const DEPRECATION_MEMBER: &str = "org.openvtc.deprecation";
pub fn note_superseded_task(task: &SupersededTask) {
counter!("deprecated_trust_task_requests_total", "task" => task.uri).increment(1);
}
pub fn annotate_superseded(body: &mut Vec<u8>, task: &SupersededTask) {
let Ok(mut doc) = serde_json::from_slice::<serde_json::Value>(body) else {
return;
};
let Some(obj) = doc.as_object_mut() else {
return;
};
if obj.contains_key("proof") || obj.contains_key(DEPRECATION_MEMBER) {
return;
}
obj.insert(
DEPRECATION_MEMBER.to_string(),
serde_json::json!({
"supersededBy": task.successor,
"reason": task.reason,
}),
);
if let Ok(bytes) = serde_json::to_vec(&doc) {
*body = bytes;
}
}
#[cfg(test)]
mod superseded_task_tests {
use super::*;
#[test]
fn a_notice_rides_the_document_top_level_not_the_payload() {
let task = &SUPERSEDED_TASKS[0];
let mut body = serde_json::to_vec(&serde_json::json!({
"id": "urn:uuid:1",
"type": "https://trusttasks.org/spec/device/list/0.1#response",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": { "devices": [] },
}))
.unwrap();
annotate_superseded(&mut body, task);
let doc: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(doc[DEPRECATION_MEMBER]["supersededBy"], task.successor);
assert_eq!(doc[DEPRECATION_MEMBER]["reason"], task.reason);
assert_eq!(doc["payload"], serde_json::json!({ "devices": [] }));
}
#[test]
fn a_signed_document_is_left_alone() {
let task = &SUPERSEDED_TASKS[0];
let original = serde_json::json!({
"id": "urn:uuid:1",
"type": "https://trusttasks.org/spec/device/list/0.1#response",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": {},
"proof": { "type": "DataIntegrityProof" },
});
let mut body = serde_json::to_vec(&original).unwrap();
annotate_superseded(&mut body, task);
let doc: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(
doc, original,
"a proofed document must be returned untouched"
);
}
#[test]
fn annotating_twice_does_not_nest_or_duplicate() {
let task = &SUPERSEDED_TASKS[0];
let mut body = serde_json::to_vec(&serde_json::json!({
"id": "urn:uuid:1",
"type": "x",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": {},
}))
.unwrap();
annotate_superseded(&mut body, task);
let once = body.clone();
annotate_superseded(&mut body, task);
assert_eq!(body, once);
}
#[test]
fn a_body_that_is_not_a_document_is_left_alone() {
let task = &SUPERSEDED_TASKS[0];
let mut empty: Vec<u8> = Vec::new();
annotate_superseded(&mut empty, task);
assert!(empty.is_empty());
let mut array = b"[1,2,3]".to_vec();
annotate_superseded(&mut array, task);
assert_eq!(array, b"[1,2,3]");
}
#[test]
fn every_row_names_a_different_successor() {
for t in SUPERSEDED_TASKS {
assert_ne!(
t.uri, t.successor,
"{} is listed as its own successor",
t.uri
);
assert!(!t.reason.is_empty(), "{} has no reason", t.uri);
}
}
#[test]
fn no_uri_is_listed_twice() {
let mut seen: Vec<&str> = SUPERSEDED_TASKS.iter().map(|t| t.uri).collect();
seen.sort_unstable();
let before = seen.len();
seen.dedup();
assert_eq!(before, seen.len(), "a URI is listed more than once");
}
}
#[cfg(test)]
mod rest_exception_tests {
use super::*;
#[test]
fn every_exception_says_why() {
for e in REST_EXCEPTIONS {
assert!(
!e.protocol.is_empty(),
"`{} {}` names no protocol",
e.method,
e.path
);
assert!(
!e.reason.is_empty(),
"`{} {}` gives no reason",
e.method,
e.path
);
}
}
}