build.rs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. //! Construction and cross-field validation of [`Config`] from raw input.
  2. //!
  3. //! Kept in a submodule so the root `config.rs` stays focused on the public
  4. //! types and loader surface.
  5. use std::{collections::HashSet, net::SocketAddr, path::Path, str::FromStr, time::Duration};
  6. use reqwest::Url;
  7. use secrecy::{ExposeSecret, SecretString};
  8. use super::{
  9. Config, ConfigError, TlsConfig,
  10. db::DbConfig,
  11. jwt::{JWT_KEY_BYTES, JwtSigningKey},
  12. policy::FailurePolicy,
  13. raw::{RawConfig, RawTls},
  14. read_secret_file,
  15. redis::RedisConfig,
  16. scope::{ScopeDefinition, TokenExchangeRule},
  17. types::{Argon2Params, RateLimitConfig, TtlConfig},
  18. };
  19. use crate::keys::{KeyId, KeyRing, KeyRingError};
  20. const DEFAULT_BIND_ADDR: &str = "0.0.0.0:8080";
  21. const DEFAULT_HEALTH_ADDR: &str = "0.0.0.0:8081";
  22. const DEFAULT_KEY_ID: &str = "v1";
  23. impl Config {
  24. pub(crate) fn build(raw: RawConfig) -> Result<Self, ConfigError> {
  25. let environment = raw.environment;
  26. validate_protected_resources(&raw)?;
  27. let issuer = parse_issuer(raw.issuer.as_deref())?;
  28. let bind_addr = parse_socket_addr(raw.bind_addr.as_deref(), DEFAULT_BIND_ADDR, |value| {
  29. ConfigError::InvalidBindAddr { value }
  30. })?;
  31. let health_addr =
  32. parse_socket_addr(raw.health_addr.as_deref(), DEFAULT_HEALTH_ADDR, |value| {
  33. ConfigError::InvalidHealthAddr { value }
  34. })?;
  35. let tls = TlsConfig::from_raw(&raw.tls)?;
  36. let keys = build_key_ring(&raw)?;
  37. let (jwt_signing_key, jwt_signing_previous_keys) = build_jwt_signing(&raw)?;
  38. let cimd_fetch_timeout = validate_cimd_fetch_timeout(raw.cimd_fetch_timeout_secs)?;
  39. let cimd_max_bytes = validate_cimd_max_bytes(raw.cimd_max_bytes)?;
  40. let database = DbConfig::from_raw(raw.database, environment)?;
  41. let redis = RedisConfig::from_raw(raw.redis)?;
  42. let failure_policy = FailurePolicy::from_raw(raw.failure_policy)?;
  43. validate_timeout_ladder(&database, &failure_policy)?;
  44. let ttl = TtlConfig::from_raw(raw.ttl)?;
  45. let argon2 = Argon2Params::from_raw(raw.argon2)?;
  46. let rate_limit = RateLimitConfig::from_raw(raw.rate_limit)?;
  47. let scope_registry = raw
  48. .scope_registry
  49. .into_iter()
  50. .map(ScopeDefinition::from_raw)
  51. .collect::<Result<Vec<_>, _>>()?;
  52. let cookie_secure = raw.cookie_secure.unwrap_or(true);
  53. if environment.is_production() && !cookie_secure {
  54. return Err(ConfigError::InsecureSessionCookie);
  55. }
  56. let cookie_domain = validate_cookie_domain(raw.cookie_domain)?;
  57. let token_exchange_allowlist = raw
  58. .token_exchange_allowlist
  59. .into_iter()
  60. .map(TokenExchangeRule::from_raw)
  61. .collect::<Result<Vec<_>, _>>()?;
  62. Ok(Self {
  63. issuer,
  64. bind_addr,
  65. health_addr,
  66. environment,
  67. tls,
  68. database,
  69. redis,
  70. keys,
  71. failure_policy,
  72. ttl,
  73. argon2,
  74. rate_limit,
  75. cookie_domain,
  76. cookie_secure,
  77. allow_open_registration: raw.allow_open_registration,
  78. pii_dual_write: raw.pii_dual_write,
  79. dcr_redirect_policy: raw.dcr_redirect_policy,
  80. cors_allowed_origins: raw.cors_allowed_origins,
  81. scope_registry,
  82. allow_token_exchange: raw.allow_token_exchange,
  83. token_exchange_allowlist,
  84. resource_indicators_enabled: raw.resource_indicators_enabled,
  85. cimd_enabled: raw.cimd_enabled,
  86. protected_resources_enabled: raw.protected_resources_enabled,
  87. protected_resource_ids: raw.protected_resource_ids,
  88. allow_localhost_redirect: raw.allow_localhost_redirect,
  89. jwt_access_tokens_enabled: raw.jwt_access_tokens_enabled,
  90. jwt_signing_key,
  91. jwt_signing_previous_keys,
  92. jwt_access_token_signing_alg: raw.jwt_access_token_signing_alg,
  93. cimd_fetch_timeout,
  94. cimd_max_bytes,
  95. })
  96. }
  97. }
  98. impl TlsConfig {
  99. fn from_raw(raw: &RawTls) -> Result<Self, ConfigError> {
  100. Ok(Self {
  101. cert_path: raw.cert_path.clone().ok_or(ConfigError::MissingTlsCert)?,
  102. key_path: raw.key_path.clone().ok_or(ConfigError::MissingTlsKey)?,
  103. client_ca_path: raw.client_ca_path.clone(),
  104. })
  105. }
  106. }
  107. fn parse_issuer(raw: Option<&str>) -> Result<String, ConfigError> {
  108. let issuer = raw
  109. .map(str::trim)
  110. .filter(|value| !value.is_empty())
  111. .ok_or(ConfigError::MissingIssuer)?;
  112. if !issuer.starts_with("https://") || issuer.ends_with('/') {
  113. return Err(ConfigError::InsecureIssuer {
  114. issuer: issuer.to_owned(),
  115. });
  116. }
  117. let authority = issuer.get("https://".len()..).unwrap_or_default();
  118. if authority.is_empty() || authority.contains(char::is_whitespace) {
  119. return Err(ConfigError::InsecureIssuer {
  120. issuer: issuer.to_owned(),
  121. });
  122. }
  123. Ok(issuer.to_owned())
  124. }
  125. fn parse_socket_addr<F>(
  126. raw: Option<&str>,
  127. default: &str,
  128. invalid: F,
  129. ) -> Result<SocketAddr, ConfigError>
  130. where
  131. F: FnOnce(String) -> ConfigError,
  132. {
  133. let value = raw
  134. .map(str::trim)
  135. .filter(|value| !value.is_empty())
  136. .unwrap_or(default);
  137. SocketAddr::from_str(value).map_err(|_| invalid(value.to_owned()))
  138. }
  139. /// Master keys must carry at least 256 bits of material.
  140. const MIN_KEY_BYTES: usize = 32;
  141. /// Reject a resolved key shorter than [`MIN_KEY_BYTES`]. `None` (no key
  142. /// configured) passes through: the key-ring chain builder reports a missing
  143. /// active key as its own error.
  144. fn require_min_key_bytes(
  145. secret: Option<SecretString>,
  146. key: &'static str,
  147. ) -> Result<Option<SecretString>, ConfigError> {
  148. match secret {
  149. Some(secret) if secret.expose_secret().len() < MIN_KEY_BYTES => {
  150. Err(ConfigError::WeakKeyMaterial { key })
  151. }
  152. other => Ok(other),
  153. }
  154. }
  155. fn build_key_ring(raw: &RawConfig) -> Result<KeyRing, ConfigError> {
  156. let pepper = require_min_key_bytes(
  157. resolve_secret(
  158. raw.token_pepper.as_deref(),
  159. raw.token_pepper_file.as_deref(),
  160. )?,
  161. "token_pepper",
  162. )?;
  163. let totp = require_min_key_bytes(
  164. resolve_secret(
  165. raw.totp_encryption_key.as_deref(),
  166. raw.totp_encryption_key_file.as_deref(),
  167. )?,
  168. "totp_encryption_key",
  169. )?;
  170. let pii = require_min_key_bytes(
  171. non_blank(resolve_secret(
  172. raw.pii_encryption_key.as_deref(),
  173. raw.pii_encryption_key_file.as_deref(),
  174. )?),
  175. "pii_encryption_key",
  176. )?;
  177. let totp_previous = require_min_key_bytes(
  178. non_blank(resolve_secret(
  179. raw.totp_previous_key.as_deref(),
  180. raw.totp_previous_key_file.as_deref(),
  181. )?),
  182. "totp_previous_key",
  183. )?;
  184. let pii_previous = require_min_key_bytes(
  185. non_blank(resolve_secret(
  186. raw.pii_previous_key.as_deref(),
  187. raw.pii_previous_key_file.as_deref(),
  188. )?),
  189. "pii_previous_key",
  190. )?;
  191. let pepper_previous = require_min_key_bytes(
  192. non_blank(resolve_secret(
  193. raw.token_pepper_previous_key.as_deref(),
  194. raw.token_pepper_previous_key_file.as_deref(),
  195. )?),
  196. "token_pepper_previous_key",
  197. )?;
  198. let pepper_id = key_id_or_default(raw.token_pepper_id.as_deref())?;
  199. let totp_id = key_id_or_default(raw.totp_key_id.as_deref())?;
  200. let pii_id = key_id_or_default(raw.pii_key_id.as_deref())?;
  201. let peppers = active_first_chain(
  202. pepper.map(|secret| (pepper_id, secret)),
  203. raw.token_pepper_previous_key_id.as_deref(),
  204. pepper_previous,
  205. KeyRingError::MissingTokenPepper,
  206. )?;
  207. let totp_keys = active_first_chain(
  208. totp.map(|secret| (totp_id, secret)),
  209. raw.totp_previous_key_id.as_deref(),
  210. totp_previous,
  211. KeyRingError::MissingTotpKey,
  212. )?;
  213. let pii_keys = active_first_chain(
  214. pii.map(|secret| (pii_id, secret)),
  215. raw.pii_previous_key_id.as_deref(),
  216. pii_previous,
  217. KeyRingError::MissingPiiKey,
  218. )?;
  219. KeyRing::from_parts(peppers, totp_keys, pii_keys).map_err(ConfigError::from)
  220. }
  221. /// Require the JWT signing key to be exactly [`JWT_KEY_BYTES`] bytes: short
  222. /// material cannot form a key and over-long material is refused (the pepper/
  223. /// TOTP/PII chains enforce only a minimum).
  224. fn require_exact_jwt_key_bytes(
  225. secret: &SecretString,
  226. key: &'static str,
  227. ) -> Result<(), ConfigError> {
  228. if secret.expose_secret().len() == JWT_KEY_BYTES {
  229. Ok(())
  230. } else {
  231. Err(ConfigError::WeakKeyMaterial { key })
  232. }
  233. }
  234. /// Build the optional active JWT signing key and its previous (verify-only)
  235. /// keys, validating length, kid uniqueness, and JOSE construction at load.
  236. fn build_jwt_signing(
  237. raw: &RawConfig,
  238. ) -> Result<(Option<JwtSigningKey>, Vec<JwtSigningKey>), ConfigError> {
  239. let alg = raw.jwt_access_token_signing_alg;
  240. let current = match non_blank(resolve_secret(
  241. raw.jwt_signing_key.as_deref(),
  242. raw.jwt_signing_key_file.as_deref(),
  243. )?) {
  244. Some(secret) => {
  245. require_exact_jwt_key_bytes(&secret, "jwt_signing_key")?;
  246. let kid = key_id_or_default(raw.jwt_signing_kid.as_deref())?;
  247. Some(JwtSigningKey::from_raw(
  248. kid,
  249. alg,
  250. &secret,
  251. "jwt_signing_key",
  252. )?)
  253. }
  254. None => None,
  255. };
  256. let mut previous = Vec::with_capacity(raw.jwt_signing_previous_keys.len());
  257. for entry in &raw.jwt_signing_previous_keys {
  258. let id_raw = entry
  259. .kid
  260. .as_deref()
  261. .map(str::trim)
  262. .filter(|value| !value.is_empty())
  263. .ok_or(ConfigError::MissingPreviousKeyId)?;
  264. let kid = validate_key_id(id_raw)?;
  265. let secret = entry
  266. .key
  267. .as_deref()
  268. .map(str::trim)
  269. .filter(|value| !value.is_empty())
  270. .ok_or(ConfigError::InvalidJwtSigningKey {
  271. key: "jwt_signing_previous_keys",
  272. })?;
  273. let secret = SecretString::from(secret.to_owned());
  274. require_exact_jwt_key_bytes(&secret, "jwt_signing_previous_keys")?;
  275. previous.push(JwtSigningKey::from_raw(
  276. kid,
  277. alg,
  278. &secret,
  279. "jwt_signing_previous_keys",
  280. )?);
  281. }
  282. if (raw.jwt_access_tokens_enabled || !previous.is_empty()) && current.is_none() {
  283. return Err(ConfigError::MissingJwtSigningKey);
  284. }
  285. let active_kid = current.as_ref().map(|key| key.kid().clone());
  286. for (index, key) in previous.iter().enumerate() {
  287. let clashes_active = active_kid.as_ref().is_some_and(|id| id == key.kid());
  288. let clashes_previous = previous[..index].iter().any(|seen| seen.kid() == key.kid());
  289. if clashes_active || clashes_previous {
  290. return Err(ConfigError::DuplicateJwtSigningKid {
  291. kid: key.kid().as_str().to_owned(),
  292. });
  293. }
  294. }
  295. Ok((current, previous))
  296. }
  297. /// RFC 9728 `protected_resources` is advertised only from a validated,
  298. /// non-empty, duplicate-free identifier set, and only when the RFC 8707
  299. /// indicator gate is also on. With the flag off the identifiers are inert.
  300. fn validate_protected_resources(raw: &RawConfig) -> Result<(), ConfigError> {
  301. if !raw.protected_resources_enabled {
  302. return Ok(());
  303. }
  304. if !raw.resource_indicators_enabled {
  305. return Err(ConfigError::InvalidProtectedResources {
  306. reason: "resource_indicators_enabled must be true".to_owned(),
  307. });
  308. }
  309. if raw.protected_resource_ids.is_empty() {
  310. return Err(ConfigError::InvalidProtectedResources {
  311. reason: "protected_resource_ids must not be empty".to_owned(),
  312. });
  313. }
  314. let mut seen = HashSet::new();
  315. for value in &raw.protected_resource_ids {
  316. let url = validate_protected_resource_id(value)?;
  317. if !seen.insert(url) {
  318. return Err(ConfigError::InvalidProtectedResources {
  319. reason: format!("duplicate protected resource id: {value}"),
  320. });
  321. }
  322. }
  323. Ok(())
  324. }
  325. /// One advertised identifier must be an absolute `https` URI with a host and
  326. /// no wildcard, userinfo, fragment, or query. The parsed record is returned so
  327. /// duplicates are detected after URL normalization.
  328. fn validate_protected_resource_id(value: &str) -> Result<Url, ConfigError> {
  329. let invalid = |reason: &str| ConfigError::InvalidProtectedResources {
  330. reason: format!("{reason}: {value}"),
  331. };
  332. if value.contains('*') {
  333. return Err(invalid("wildcards are not allowed"));
  334. }
  335. let url = Url::parse(value).map_err(|_| invalid("must be an absolute URI"))?;
  336. if url.scheme() != "https" || url.host_str().is_none_or(str::is_empty) {
  337. return Err(invalid("must be an absolute https URI with a host"));
  338. }
  339. if !url.username().is_empty() || url.password().is_some() {
  340. return Err(invalid("userinfo is not allowed"));
  341. }
  342. if url.fragment().is_some() {
  343. return Err(invalid("a fragment is not allowed"));
  344. }
  345. if url.query().is_some() {
  346. return Err(invalid("a query is not allowed"));
  347. }
  348. Ok(url)
  349. }
  350. /// CIMD fetch timeout must sit inside the 1-10 second window.
  351. fn validate_cimd_fetch_timeout(secs: u64) -> Result<Duration, ConfigError> {
  352. if (1..=10).contains(&secs) {
  353. Ok(Duration::from_secs(secs))
  354. } else {
  355. Err(ConfigError::InvalidCimdFetchTimeout)
  356. }
  357. }
  358. /// CIMD document byte cap must be positive and at most 5120 bytes.
  359. fn validate_cimd_max_bytes(bytes: usize) -> Result<usize, ConfigError> {
  360. if (1..=5_120).contains(&bytes) {
  361. Ok(bytes)
  362. } else {
  363. Err(ConfigError::InvalidCimdMaxBytes)
  364. }
  365. }
  366. /// Build an active-first chain from the active key plus its retained previous
  367. /// key. A missing active key with no previous key yields an empty chain (the
  368. /// caller maps it to `missing_active`); a previous key without its active key
  369. /// (R2) or without an explicit distinct id (R1) is rejected here so a retained
  370. /// key can never silently become active.
  371. fn active_first_chain(
  372. active: Option<(KeyId, SecretString)>,
  373. previous_id_raw: Option<&str>,
  374. previous: Option<SecretString>,
  375. missing_active: KeyRingError,
  376. ) -> Result<Vec<(KeyId, SecretString)>, ConfigError> {
  377. match (active, previous) {
  378. (None, None) => Ok(Vec::new()),
  379. (Some(pair), None) => Ok(vec![pair]),
  380. (None, Some(_)) => Err(ConfigError::KeyRing(missing_active)),
  381. (Some(active_pair), Some(previous_secret)) => {
  382. let id_raw = previous_id_raw
  383. .map(str::trim)
  384. .filter(|value| !value.is_empty())
  385. .ok_or(ConfigError::MissingPreviousKeyId)?;
  386. let id = validate_key_id(id_raw)?;
  387. if id == active_pair.0 {
  388. return Err(ConfigError::MissingPreviousKeyId);
  389. }
  390. Ok(vec![active_pair, (id, previous_secret)])
  391. }
  392. }
  393. }
  394. /// Treat empty/whitespace-only resolved material as absent. The inline path in
  395. /// `resolve_secret` already drops blanks, but a mounted file can be zero-byte
  396. /// or whitespace-only; such a value must not become a chain element.
  397. fn non_blank(secret: Option<SecretString>) -> Option<SecretString> {
  398. secret.filter(|value| !value.expose_secret().trim().is_empty())
  399. }
  400. fn resolve_secret(
  401. inline: Option<&str>,
  402. file: Option<&Path>,
  403. ) -> Result<Option<SecretString>, ConfigError> {
  404. match inline.map(str::trim).filter(|value| !value.is_empty()) {
  405. Some(value) => Ok(Some(SecretString::from(value.to_owned()))),
  406. None => read_secret_file(file),
  407. }
  408. }
  409. fn key_id_or_default(raw: Option<&str>) -> Result<KeyId, ConfigError> {
  410. match raw.map(str::trim).filter(|value| !value.is_empty()) {
  411. Some(value) => validate_key_id(value),
  412. None => Ok(KeyId::new(DEFAULT_KEY_ID)),
  413. }
  414. }
  415. /// Reject a configured key id outside `[A-Za-z0-9_-]+`: `.` would collide
  416. /// with the `<kid>.<digest>` storage format, and empty/whitespace values are
  417. /// never valid ids (absent ids fall back to the default above).
  418. fn validate_key_id(value: &str) -> Result<KeyId, ConfigError> {
  419. if crate::keys::is_valid_key_id(value) {
  420. Ok(KeyId::new(value))
  421. } else {
  422. Err(ConfigError::InvalidKeyId {
  423. id: value.to_owned(),
  424. })
  425. }
  426. }
  427. fn validate_timeout_ladder(db: &DbConfig, policy: &FailurePolicy) -> Result<(), ConfigError> {
  428. let lock = db.lock_timeout;
  429. let statement = db.statement_timeout;
  430. let acquire = db.acquire_timeout;
  431. let request = policy.request_timeout;
  432. if lock < statement && statement <= acquire && acquire < request {
  433. Ok(())
  434. } else {
  435. Err(ConfigError::InvalidTimeoutLadder)
  436. }
  437. }
  438. /// Validate `cookie_domain` against the issuer host (T3-D2).
  439. ///
  440. /// Every non-blank value is rejected: the `__Host-` session-cookie prefix
  441. /// forbids `Domain`, so sessions stay host-only by design and the key remains
  442. /// reserved. Blank values are treated as absent, matching the other optional
  443. /// keys.
  444. fn validate_cookie_domain(raw: Option<String>) -> Result<Option<String>, ConfigError> {
  445. let Some(domain) = raw else {
  446. return Ok(None);
  447. };
  448. let normalized = domain.trim().trim_start_matches('.').to_ascii_lowercase();
  449. if normalized.is_empty() {
  450. return Ok(None);
  451. }
  452. Err(ConfigError::InvalidCookieDomain { value: domain })
  453. }