An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(relay): address PR review issues for MM-92

- Issue #1 (Critical): Replace non-constant-time Bearer token comparison with subtle::ConstantTimeEq to prevent timing attacks in create_signing_key.rs:57
- Issue #2 (Critical): Move zeroize and subtle dependencies to [workspace.dependencies] in root Cargo.toml; update crates to use { workspace = true } per project conventions
- Issue #3 (High): Fix migration infrastructure to return DbError instead of silently mapping corrupt schema_migrations version numbers to 0; now propagates parse errors with ? operator in mod.rs:99-107
- Issue #4 (High): Add sentinel field signing_key_master_key_toml_sentinel to RawConfig to detect and reject misconfigured operators who set the security-sensitive field in relay.toml instead of env var EZPDS_SIGNING_KEY_MASTER_KEY; includes validation check and regression test in config.rs

authored by

Malpercio and committed by
Tangled
6ab3ce66 279c7d12

+43 -6
+2
Cargo.toml
··· 59 59 multibase = "0.9" 60 60 rand_core = { version = "0.6", features = ["getrandom"] } 61 61 base64 = "0.21" 62 + zeroize = "1" 63 + subtle = "2" 62 64 63 65 # Testing 64 66 tempfile = "3"
+28
crates/common/src/config.rs
··· 104 104 pub(crate) admin_token: Option<String>, 105 105 #[serde(skip)] 106 106 pub(crate) signing_key_master_key: Option<[u8; 32]>, 107 + /// Sentinel field — only present to detect misconfiguration. 108 + /// signing_key_master_key must be set via env var EZPDS_SIGNING_KEY_MASTER_KEY, not TOML. 109 + #[serde(rename = "signing_key_master_key")] 110 + pub(crate) signing_key_master_key_toml_sentinel: Option<String>, 107 111 } 108 112 109 113 #[derive(Debug, thiserror::Error)] ··· 232 236 /// When provided, `telemetry.otlp_endpoint` must be non-empty and start with `http://` or 233 237 /// `https://`. 234 238 pub(crate) fn validate_and_build(raw: RawConfig) -> Result<Config, ConfigError> { 239 + // Reject signing_key_master_key if it appears in TOML (must be env var only). 240 + if raw.signing_key_master_key_toml_sentinel.is_some() { 241 + return Err(ConfigError::Invalid( 242 + "signing_key_master_key must be set via env var EZPDS_SIGNING_KEY_MASTER_KEY, not relay.toml (security-sensitive field)".to_string() 243 + )); 244 + } 245 + 235 246 let bind_address = raw.bind_address.unwrap_or_else(|| "0.0.0.0".to_string()); 236 247 let port = raw.port.unwrap_or(8080); 237 248 let data_dir: PathBuf = raw ··· 773 784 invalid_key.to_string(), 774 785 )]); 775 786 let err = apply_env_overrides(minimal_raw(), &env).unwrap_err(); 787 + assert!(matches!(err, ConfigError::Invalid(_))); 788 + assert!(err.to_string().contains("EZPDS_SIGNING_KEY_MASTER_KEY")); 789 + } 790 + 791 + #[test] 792 + fn signing_key_master_key_in_toml_returns_error() { 793 + // Operator mistakenly puts signing_key_master_key in relay.toml instead of env var. 794 + // The sentinel field must catch this and reject the configuration. 795 + let toml = r#" 796 + data_dir = "/var/pds" 797 + public_url = "https://pds.example.com" 798 + available_user_domains = ["example.com"] 799 + signing_key_master_key = "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" 800 + "#; 801 + let raw: RawConfig = toml::from_str(toml).unwrap(); 802 + let err = validate_and_build(raw).unwrap_err(); 803 + 776 804 assert!(matches!(err, ConfigError::Invalid(_))); 777 805 assert!(err.to_string().contains("EZPDS_SIGNING_KEY_MASTER_KEY")); 778 806 }
+1 -1
crates/crypto/Cargo.toml
··· 14 14 rand_core = { workspace = true } 15 15 base64 = { workspace = true } 16 16 thiserror = { workspace = true } 17 - zeroize = "1" 17 + zeroize = { workspace = true }
+1
crates/relay/Cargo.toml
··· 27 27 serde = { workspace = true } 28 28 sqlx = { workspace = true } 29 29 crypto = { workspace = true } 30 + subtle = { workspace = true } 30 31 31 32 [dev-dependencies] 32 33 tower = { workspace = true }
+4 -4
crates/relay/src/db/mod.rs
··· 99 99 let applied_set: std::collections::HashSet<u32> = applied 100 100 .into_iter() 101 101 .map(|(v,)| { 102 - u32::try_from(v).unwrap_or_else(|_| { 103 - tracing::warn!(version = v, "ignoring out-of-range migration version"); 104 - 0 102 + u32::try_from(v).map_err(|_| DbError::Setup { 103 + step: "parse migration version from schema_migrations", 104 + source: sqlx::Error::RowNotFound, 105 105 }) 106 106 }) 107 - .collect(); 107 + .collect::<Result<_, _>>()?; 108 108 109 109 // Collect pending migrations in order. 110 110 let pending: Vec<&Migration> = MIGRATIONS
+7 -1
crates/relay/src/routes/create_signing_key.rs
··· 6 6 7 7 use axum::{extract::State, http::HeaderMap, response::Json}; 8 8 use serde::{Deserialize, Serialize}; 9 + use subtle::ConstantTimeEq; 9 10 10 11 use common::{ApiError, ErrorCode}; 11 12 ··· 54 55 ) 55 56 })?; 56 57 57 - if provided_token != expected_token { 58 + if provided_token 59 + .as_bytes() 60 + .ct_eq(expected_token.as_bytes()) 61 + .unwrap_u8() 62 + != 1 63 + { 58 64 return Err(ApiError::new( 59 65 ErrorCode::Unauthorized, 60 66 "invalid admin token",