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;
26pub mod router;
27
28pub use extractor::TrustTaskHeader;
29pub use router::TrustTaskRouter;
30
31use crate::error::AppError;
32
33/// Canonical HTTP header name carrying the Trust-Task identifier on
34/// REST requests. The workspace pins this literal so a future audit
35/// can grep for header consumers without ambiguity.
36pub const HEADER_NAME: &str = "Trust-Task";
37
38/// A validated Trust-Task identifier.
39///
40/// The workspace treats Trust-Task URLs as opaque — we don't enforce
41/// the full `https://trusttasks.org/{org}/{path}/{maj}.{min}` shape
42/// because the registry's canonical format is still evolving (spec
43/// §17 Q10). What we **do** enforce:
44///
45/// - non-empty
46/// - starts with `https://`
47/// - no CR/LF characters (prevents header-injection attacks via a
48///   round-tripped Trust-Task value)
49///
50/// Exact-match against a handler's registered task is the only
51/// correctness check at request time — see
52/// [`TrustTaskRouter::route_with_task`].
53#[derive(Debug, Clone, PartialEq, Eq, Hash)]
54pub struct TrustTask(String);
55
56impl TrustTask {
57    /// Parse and validate a Trust-Task identifier. Returns
58    /// [`AppError::TrustTaskMalformed`] for empty, non-HTTPS, or
59    /// control-character-containing values.
60    pub fn new(s: impl Into<String>) -> Result<Self, AppError> {
61        let s = s.into();
62        if s.is_empty() {
63            return Err(AppError::TrustTaskMalformed("<empty>".into()));
64        }
65        if !s.starts_with("https://") {
66            return Err(AppError::TrustTaskMalformed(s));
67        }
68        if s.chars().any(|c| c == '\r' || c == '\n' || c == '\0') {
69            return Err(AppError::TrustTaskMalformed(s));
70        }
71        Ok(Self(s))
72    }
73
74    /// The validated identifier as a `&str`.
75    pub fn as_str(&self) -> &str {
76        &self.0
77    }
78}
79
80impl AsRef<str> for TrustTask {
81    fn as_ref(&self) -> &str {
82        &self.0
83    }
84}
85
86impl std::fmt::Display for TrustTask {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.write_str(&self.0)
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn accepts_well_formed_https_url() {
98        let t = TrustTask::new("https://trusttasks.org/openvtc/vtc/install/claim/1.0").unwrap();
99        assert_eq!(
100            t.as_str(),
101            "https://trusttasks.org/openvtc/vtc/install/claim/1.0"
102        );
103    }
104
105    #[test]
106    fn rejects_empty_string() {
107        let err = TrustTask::new("").expect_err("empty");
108        assert!(matches!(err, AppError::TrustTaskMalformed(_)));
109    }
110
111    #[test]
112    fn rejects_non_https() {
113        for s in [
114            "http://trusttasks.org/openvtc/vtc/install/claim/1.0",
115            "urn:openvtc:vtc:install:claim:1.0",
116            "trusttasks.org/openvtc/vtc/install/claim/1.0",
117        ] {
118            let err = TrustTask::new(s).expect_err("non-https");
119            assert!(
120                matches!(err, AppError::TrustTaskMalformed(_)),
121                "{s} should be rejected"
122            );
123        }
124    }
125
126    #[test]
127    fn rejects_header_injection_attempts() {
128        for s in [
129            "https://trusttasks.org/x\r\nInjected: yes",
130            "https://trusttasks.org/x\nInjected: yes",
131            "https://trusttasks.org/x\0",
132        ] {
133            let err = TrustTask::new(s).expect_err("control chars");
134            assert!(
135                matches!(err, AppError::TrustTaskMalformed(_)),
136                "{s:?} should be rejected"
137            );
138        }
139    }
140
141    #[test]
142    fn display_returns_full_url() {
143        let t = TrustTask::new("https://trusttasks.org/x/1.0").unwrap();
144        assert_eq!(format!("{t}"), "https://trusttasks.org/x/1.0");
145    }
146}