Skip to main content

vti_common/
config.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Deserialize, Serialize)]
5pub struct ServerConfig {
6    #[serde(default = "default_host")]
7    pub host: String,
8    /// Port number. No default — each service must provide its own via
9    /// `#[serde(default = "...")]` or by composing this struct.
10    pub port: u16,
11}
12
13#[derive(Debug, Clone, Deserialize, Serialize)]
14pub struct LogConfig {
15    #[serde(default = "default_log_level")]
16    pub level: String,
17    #[serde(default)]
18    pub format: LogFormat,
19}
20
21#[derive(Debug, Clone, Deserialize, Serialize)]
22pub struct StoreConfig {
23    /// Data directory. No default — each service provides its own
24    /// (e.g., "data/vta" vs "data/vtc").
25    pub data_dir: PathBuf,
26}
27
28#[derive(Clone, Deserialize, Serialize)]
29pub struct AuthConfig {
30    #[serde(default = "default_access_token_expiry")]
31    pub access_token_expiry: u64,
32    #[serde(default = "default_refresh_token_expiry")]
33    pub refresh_token_expiry: u64,
34    #[serde(default = "default_challenge_ttl")]
35    pub challenge_ttl: u64,
36    #[serde(default = "default_session_cleanup_interval")]
37    pub session_cleanup_interval: u64,
38    /// Base64url-no-pad encoded 32-byte Ed25519 private key for JWT signing.
39    pub jwt_signing_key: Option<String>,
40    /// AAL2 step-up policy. Defaults to disabled (AAL1 everywhere) — a fresh
41    /// VTA has no approver registered, so step-up is opt-in once one exists.
42    /// See `auth/step-up/policy/0.1`.
43    #[serde(default)]
44    pub step_up: crate::auth::step_up::StepUpPolicy,
45}
46
47// Manual Debug so a `tracing::debug!(?config, ...)`, panic-with-debug,
48// or `format!("{:?}", app_config)` in a downstream crate cannot dump
49// the JWT signing key into logs (which in enclave mode are forwarded
50// over vsock to the host). Non-secret fields stay visible for
51// diagnostics; `Serialize` is intentionally untouched since these
52// structs round-trip to the on-disk config file.
53impl std::fmt::Debug for AuthConfig {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.debug_struct("AuthConfig")
56            .field("access_token_expiry", &self.access_token_expiry)
57            .field("refresh_token_expiry", &self.refresh_token_expiry)
58            .field("challenge_ttl", &self.challenge_ttl)
59            .field("session_cleanup_interval", &self.session_cleanup_interval)
60            .field(
61                "jwt_signing_key",
62                &self.jwt_signing_key.as_ref().map(|_| "<redacted>"),
63            )
64            .field("step_up", &self.step_up)
65            .finish()
66    }
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize)]
70pub struct MessagingConfig {
71    /// Mediator URL. Optional — the TDK resolves the endpoint from mediator_did.
72    /// Kept for display/status purposes and backward compatibility.
73    #[serde(default)]
74    pub mediator_url: String,
75    pub mediator_did: String,
76    /// Real external hostname of the mediator (e.g., "mediator.example.com").
77    /// Used by the parent proxy to establish the TLS connection.
78    /// Not used by the VTA itself (which connects via the local vsock proxy).
79    #[serde(default)]
80    pub mediator_host: Option<String>,
81    /// Automatically provision a per-DID allow-all ACL on the mediator after
82    /// establishing the DIDComm connection. Required when the mediator uses
83    /// `ExplicitAllow` mode; harmless (and default-off) with `ExplicitDeny`.
84    /// Set `setup_acl = true` during setup to enable. Defaults to `false`.
85    #[serde(default)]
86    pub setup_acl: bool,
87    /// Drain this DID's mediator inbox over REST at startup, *before* the live
88    /// DIDComm/TSP listener enables live delivery.
89    ///
90    /// Recovery lever for a wedged listener: the mediator enforces one live
91    /// websocket stream per DID, and an undeliverable/poison message queued for
92    /// this DID can stall the live-delivery handshake so the listener never comes
93    /// up (taking DIDComm *and* TSP down, since they share the socket). Because
94    /// REST auth + pickup work even when the websocket stalls, the VTA can fetch
95    /// and clear its own queued messages first: each is best-effort processed,
96    /// and anything that fails to unpack/handle is logged loudly and deleted so
97    /// it can't wedge startup again.
98    ///
99    /// **Default off** — it deletes queued messages that can't be handled, so it
100    /// is opt-in. Turn it on when a mediator-side backlog is blocking boot.
101    #[serde(default)]
102    pub drain_inbox_on_start: bool,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct AuditConfig {
107    /// Number of days to retain audit logs (default 28).
108    #[serde(default = "default_audit_retention_days")]
109    pub retention_days: u32,
110}
111
112fn default_audit_retention_days() -> u32 {
113    28
114}
115
116impl Default for AuditConfig {
117    fn default() -> Self {
118        Self {
119            retention_days: default_audit_retention_days(),
120        }
121    }
122}
123
124/// Vault lifecycle tuning. Shared shape so both the VTA password vault and
125/// the VTA credential store read the same grace window.
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct VaultConfig {
128    /// Days a soft-deleted (tombstoned) vault entry or credential remains
129    /// recoverable before the sweeper hard-purges it. Applied at delete time
130    /// (`grace_until = now + grace_days`); the sweeper only compares against
131    /// the stored `grace_until`. Default 30. A `delete --force` / `purge`
132    /// bypasses the window entirely.
133    #[serde(default = "default_vault_grace_days")]
134    pub grace_days: u32,
135}
136
137fn default_vault_grace_days() -> u32 {
138    30
139}
140
141impl Default for VaultConfig {
142    fn default() -> Self {
143        Self {
144            grace_days: default_vault_grace_days(),
145        }
146    }
147}
148
149#[derive(Debug, Default, Deserialize, Serialize, Clone, PartialEq)]
150#[serde(rename_all = "lowercase")]
151pub enum LogFormat {
152    #[default]
153    Text,
154    Json,
155}
156
157fn default_host() -> String {
158    "0.0.0.0".to_string()
159}
160
161fn default_log_level() -> String {
162    "info".to_string()
163}
164
165fn default_access_token_expiry() -> u64 {
166    900
167}
168
169fn default_refresh_token_expiry() -> u64 {
170    86400
171}
172
173fn default_challenge_ttl() -> u64 {
174    300
175}
176
177fn default_session_cleanup_interval() -> u64 {
178    600
179}
180
181impl Default for AuthConfig {
182    fn default() -> Self {
183        Self {
184            access_token_expiry: default_access_token_expiry(),
185            refresh_token_expiry: default_refresh_token_expiry(),
186            challenge_ttl: default_challenge_ttl(),
187            session_cleanup_interval: default_session_cleanup_interval(),
188            jwt_signing_key: None,
189            step_up: crate::auth::step_up::StepUpPolicy::default(),
190        }
191    }
192}
193
194impl Default for LogConfig {
195    fn default() -> Self {
196        Self {
197            level: default_log_level(),
198            format: LogFormat::default(),
199        }
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    /// `AuthConfig`'s Debug impl MUST NOT print the JWT signing key —
208    /// it's the Ed25519 private key used to sign every access token. A
209    /// stray `tracing::debug!(?config, ...)` or panic-with-debug
210    /// formatter would otherwise dump it into logs.
211    #[test]
212    fn auth_config_debug_redacts_jwt_signing_key() {
213        let cfg = AuthConfig {
214            access_token_expiry: 900,
215            refresh_token_expiry: 86400,
216            challenge_ttl: 300,
217            session_cleanup_interval: 600,
218            jwt_signing_key: Some("SUPER_SECRET_KEY_MATERIAL_MUST_NOT_LEAK".into()),
219            step_up: Default::default(),
220        };
221        let dbg = format!("{cfg:?}");
222        assert!(
223            !dbg.contains("SUPER_SECRET_KEY_MATERIAL"),
224            "AuthConfig Debug leaked jwt_signing_key contents: {dbg}"
225        );
226        assert!(
227            dbg.contains("<redacted>"),
228            "expected redaction marker in Debug, got: {dbg}"
229        );
230        // Non-secret fields must remain visible for diagnostics.
231        assert!(
232            dbg.contains("900"),
233            "access_token_expiry must still be visible: {dbg}"
234        );
235    }
236
237    #[test]
238    fn auth_config_debug_none_signing_key_renders_none() {
239        let cfg = AuthConfig::default();
240        let dbg = format!("{cfg:?}");
241        // `Option<&str>` Debug prints `None` for the absent case.
242        assert!(dbg.contains("jwt_signing_key: None"), "got: {dbg}");
243    }
244
245    /// Serialize must remain unaffected — these structs round-trip to
246    /// the config file, and redacting them on serialize would break
247    /// persistence. Use JSON here since serde_json is already a
248    /// dev-dep; the wire format (TOML on disk) shares the same serde
249    /// derive so this is sufficient to prove non-redaction.
250    #[test]
251    fn auth_config_serialize_still_carries_jwt_signing_key() {
252        let cfg = AuthConfig {
253            access_token_expiry: 900,
254            refresh_token_expiry: 86400,
255            challenge_ttl: 300,
256            session_cleanup_interval: 600,
257            jwt_signing_key: Some("key-material".into()),
258            step_up: Default::default(),
259        };
260        let json = serde_json::to_string(&cfg).expect("serialize");
261        assert!(
262            json.contains("key-material"),
263            "Serialize must not redact — config persistence relies on round-trip: {json}"
264        );
265    }
266}