Skip to main content

vti_common/trust_task/
mod.rs

1//! Trust-Task primitive — every wire op in the workspace binds to a
2//! versioned Trust Task identifier published on
3//! [`trusttasks.org`](https://trusttasks.org). See spec §3-L and §16 of
4//! `docs/05-design-notes/vtc-mvp.md` for the full design rationale.
5//!
6//! This module ships the workspace-wide foundation:
7//!
8//! - [`TrustTask`] — a validated newtype around the Trust-Task
9//!   identifier (a URL the workspace treats as opaque).
10//! - [`HEADER_NAME`] — the canonical HTTP header name (`Trust-Task`).
11//! - [`extractor::TrustTaskHeader`] — Axum extractor for handlers that
12//!   want to read the header value directly.
13//! - [`router::TrustTaskRouter`] — builder that wraps Axum `Router`
14//!   and enforces exact-match Trust-Task header validation **at route
15//!   attach time** (no string-prefix tricks, no version-family
16//!   matching — see spec §9.4).
17//!
18//! ## Design call
19//!
20//! The router builder is explicit and macro-free per the M0.1.1 plan
21//! decision **D9**. A future-reader sees the registered task right
22//! next to the handler in source, and `cargo doc` surfaces it on the
23//! route without any procedural-macro indirection.
24
25pub mod extractor;
26#[cfg(feature = "openapi")]
27pub mod openapi;
28pub mod router;
29
30pub use extractor::TrustTaskHeader;
31#[cfg(feature = "openapi")]
32pub use openapi::{task_layer, task_routes};
33pub use router::TrustTaskRouter;
34
35use crate::error::AppError;
36
37/// Canonical HTTP header name carrying the Trust-Task identifier on
38/// REST requests. The workspace pins this literal so a future audit
39/// can grep for header consumers without ambiguity.
40pub const HEADER_NAME: &str = "Trust-Task";
41
42/// A validated Trust-Task identifier.
43///
44/// The workspace treats Trust-Task URLs as opaque — we don't enforce
45/// the full `https://trusttasks.org/{org}/{path}/{maj}.{min}` shape
46/// because the registry's canonical format is still evolving (spec
47/// §17 Q10). What we **do** enforce:
48///
49/// - non-empty
50/// - starts with `https://`
51/// - no CR/LF characters (prevents header-injection attacks via a
52///   round-tripped Trust-Task value)
53///
54/// Exact-match against a handler's registered task is the only
55/// correctness check at request time — see
56/// [`TrustTaskRouter::route_with_task`].
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct TrustTask(String);
59
60impl TrustTask {
61    /// Parse and validate a Trust-Task identifier. Returns
62    /// [`AppError::TrustTaskMalformed`] for empty, non-HTTPS, or
63    /// control-character-containing values.
64    pub fn new(s: impl Into<String>) -> Result<Self, AppError> {
65        let s = s.into();
66        if s.is_empty() {
67            return Err(AppError::TrustTaskMalformed("<empty>".into()));
68        }
69        if !s.starts_with("https://") {
70            return Err(AppError::TrustTaskMalformed(s));
71        }
72        if s.chars().any(|c| c == '\r' || c == '\n' || c == '\0') {
73            return Err(AppError::TrustTaskMalformed(s));
74        }
75        Ok(Self(s))
76    }
77
78    /// The validated identifier as a `&str`.
79    pub fn as_str(&self) -> &str {
80        &self.0
81    }
82}
83
84impl AsRef<str> for TrustTask {
85    fn as_ref(&self) -> &str {
86        &self.0
87    }
88}
89
90impl std::fmt::Display for TrustTask {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        f.write_str(&self.0)
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn accepts_well_formed_https_url() {
102        let t = TrustTask::new("https://trusttasks.org/spec/vtc/install/claim/0.1").unwrap();
103        assert_eq!(
104            t.as_str(),
105            "https://trusttasks.org/spec/vtc/install/claim/0.1"
106        );
107    }
108
109    #[test]
110    fn rejects_empty_string() {
111        let err = TrustTask::new("").expect_err("empty");
112        assert!(matches!(err, AppError::TrustTaskMalformed(_)));
113    }
114
115    #[test]
116    fn rejects_non_https() {
117        for s in [
118            "http://trusttasks.org/spec/vtc/install/claim/0.1",
119            "urn:openvtc:vtc:install:claim:1.0",
120            "trusttasks.org/spec/vtc/install/claim/0.1",
121        ] {
122            let err = TrustTask::new(s).expect_err("non-https");
123            assert!(
124                matches!(err, AppError::TrustTaskMalformed(_)),
125                "{s} should be rejected"
126            );
127        }
128    }
129
130    #[test]
131    fn rejects_header_injection_attempts() {
132        for s in [
133            "https://trusttasks.org/x\r\nInjected: yes",
134            "https://trusttasks.org/x\nInjected: yes",
135            "https://trusttasks.org/x\0",
136        ] {
137            let err = TrustTask::new(s).expect_err("control chars");
138            assert!(
139                matches!(err, AppError::TrustTaskMalformed(_)),
140                "{s:?} should be rejected"
141            );
142        }
143    }
144
145    #[test]
146    fn display_returns_full_url() {
147        let t = TrustTask::new("https://trusttasks.org/x/1.0").unwrap();
148        assert_eq!(format!("{t}"), "https://trusttasks.org/x/1.0");
149    }
150}