reactor(opaque): normalize to OXICLOUD_AUTH_OPAQUE_* variables
This commit is contained in:
@@ -181,7 +181,7 @@ pub struct AuthApplicationService {
|
||||
/// `AuthConfig::require_verified_email`.
|
||||
require_verified_email: bool,
|
||||
/// OPAQUE envelope repo — populated when the OPAQUE substrate is
|
||||
/// wired (`OXICLOUD_OPAQUE_MODE != off`). `login()` consults it to
|
||||
/// wired (`OXICLOUD_AUTH_OPAQUE_MODE != off`). `login()` consults it to
|
||||
/// enforce the Phase 4 gate: once a user has completed at least
|
||||
/// one successful OPAQUE handshake (`opaque_migrated_at IS NOT
|
||||
/// NULL`), legacy `POST /api/auth/login` is refused for that
|
||||
@@ -366,7 +366,7 @@ impl AuthApplicationService {
|
||||
}
|
||||
|
||||
/// Wire the OPAQUE envelope repo. Called by the DI factory when the
|
||||
/// OPAQUE substrate is configured (`OXICLOUD_OPAQUE_MODE != off`).
|
||||
/// OPAQUE substrate is configured (`OXICLOUD_AUTH_OPAQUE_MODE != off`).
|
||||
/// Enables the Phase 4 legacy-login gate — see the field docstring.
|
||||
pub fn with_opaque_repo(
|
||||
mut self,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! `opaque-setup` — one-shot operator helper that mints a fresh
|
||||
//! [`opaque_ke::ServerSetup`] and prints its base64 encoding to stdout.
|
||||
//!
|
||||
//! The output goes into `OXICLOUD_OPAQUE_SERVER_SETUP` (env var or secrets
|
||||
//! The output goes into `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` (env var or secrets
|
||||
//! manager) and MUST be persisted verbatim. Rotating it invalidates every
|
||||
//! user's registration — treat it like the JWT secret, only more so.
|
||||
//!
|
||||
@@ -9,7 +9,7 @@
|
||||
//! ```text
|
||||
//! cargo run --bin opaque-setup > opaque_setup.b64
|
||||
//! # or paste directly into your env / .env file:
|
||||
//! echo "OXICLOUD_OPAQUE_SERVER_SETUP=$(cargo run --bin opaque-setup)" >> .env
|
||||
//! echo "OXICLOUD_AUTH_OPAQUE_SERVER_SETUP=$(cargo run --bin opaque-setup)" >> .env
|
||||
//! ```
|
||||
//!
|
||||
//! The generated value is a small (~64 byte) Ristretto255 keypair
|
||||
@@ -22,13 +22,13 @@ use oxicloud::infrastructure::services::opaque_service::OpaqueService;
|
||||
fn main() {
|
||||
let b64 = OpaqueService::generate_server_setup_b64();
|
||||
// Print JUST the value — no trailing newline commentary — so shell
|
||||
// pipelines (`OXICLOUD_OPAQUE_SERVER_SETUP=$(cargo run --bin opaque-setup)`)
|
||||
// pipelines (`OXICLOUD_AUTH_OPAQUE_SERVER_SETUP=$(cargo run --bin opaque-setup)`)
|
||||
// capture cleanly without needing `tr -d '\n'` afterwards.
|
||||
println!("{b64}");
|
||||
// Guidance goes to stderr so it doesn't contaminate the pipeline.
|
||||
eprintln!();
|
||||
eprintln!("=== OPAQUE server setup generated. ===");
|
||||
eprintln!("Persist the line above in OXICLOUD_OPAQUE_SERVER_SETUP.");
|
||||
eprintln!("Persist the line above in OXICLOUD_AUTH_OPAQUE_SERVER_SETUP.");
|
||||
eprintln!("NEVER rotate: rotating invalidates every user's registration.");
|
||||
eprintln!("Treat this value like your JWT secret.");
|
||||
}
|
||||
|
||||
+16
-16
@@ -1667,17 +1667,17 @@ pub struct OpaqueConfig {
|
||||
/// [`crate::infrastructure::services::opaque_service::OpaqueMode`]
|
||||
/// for the state-machine and the phase-plan mapping.
|
||||
///
|
||||
/// Env: `OXICLOUD_OPAQUE_MODE` (`off` | `migrate` | `opaque_only`).
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_MODE` (`off` | `migrate` | `opaque_only`).
|
||||
/// Default: `off`.
|
||||
pub mode: crate::infrastructure::services::opaque_service::OpaqueMode,
|
||||
/// Base64-encoded [`opaque_ke::ServerSetup`] blob. Generated once
|
||||
/// per deployment and persisted verbatim — rotating this invalidates
|
||||
/// every user's registration. Runbook: on first boot with
|
||||
/// `OXICLOUD_OPAQUE_MODE != off`, if this is unset, print a fatal
|
||||
/// `OXICLOUD_AUTH_OPAQUE_MODE != off`, if this is unset, print a fatal
|
||||
/// message with a fresh setup for the operator to paste into their
|
||||
/// env, then exit.
|
||||
///
|
||||
/// Env: `OXICLOUD_OPAQUE_SERVER_SETUP`. No default.
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP`. No default.
|
||||
pub server_setup_b64: Option<String>,
|
||||
/// Ciphersuite version stamped into `auth.users.opaque_ciphersuite_version`
|
||||
/// on registration. Bumping this without changing the actual
|
||||
@@ -1690,15 +1690,15 @@ pub struct OpaqueConfig {
|
||||
/// the client can construct a matching `argon2::Argon2` before
|
||||
/// running `ClientRegistration::start` / `ClientLogin::start`.
|
||||
///
|
||||
/// Env: `OXICLOUD_OPAQUE_KSF_MEMORY_KIB`. Default: `262144` (256 MiB).
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB`. Default: `262144` (256 MiB).
|
||||
pub ksf_memory_kib: u32,
|
||||
/// Client-side Argon2id iteration count.
|
||||
///
|
||||
/// Env: `OXICLOUD_OPAQUE_KSF_ITERATIONS`. Default: `3`.
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS`. Default: `3`.
|
||||
pub ksf_iterations: u32,
|
||||
/// Client-side Argon2id parallelism (lanes).
|
||||
///
|
||||
/// Env: `OXICLOUD_OPAQUE_KSF_PARALLELISM`. Default: `4`.
|
||||
/// Env: `OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM`. Default: `4`.
|
||||
pub ksf_parallelism: u32,
|
||||
}
|
||||
|
||||
@@ -1723,33 +1723,33 @@ impl OpaqueConfig {
|
||||
pub fn from_env() -> Self {
|
||||
use std::env;
|
||||
let mut cfg = Self::default();
|
||||
if let Ok(v) = env::var("OXICLOUD_OPAQUE_MODE") {
|
||||
if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_MODE") {
|
||||
match crate::infrastructure::services::opaque_service::OpaqueMode::parse(&v) {
|
||||
Some(m) => cfg.mode = m,
|
||||
None => {
|
||||
tracing::warn!(
|
||||
target: "oxicloud::config",
|
||||
value = %v,
|
||||
"OXICLOUD_OPAQUE_MODE has an unrecognised value — keeping default (off). \
|
||||
"OXICLOUD_AUTH_OPAQUE_MODE has an unrecognised value — keeping default (off). \
|
||||
Accepted: off | migrate | opaque_only"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Ok(v) = env::var("OXICLOUD_OPAQUE_SERVER_SETUP") {
|
||||
if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_SERVER_SETUP") {
|
||||
cfg.server_setup_b64 = Some(v);
|
||||
}
|
||||
if let Ok(v) = env::var("OXICLOUD_OPAQUE_KSF_MEMORY_KIB")
|
||||
if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_KSF_MEMORY_KIB")
|
||||
&& let Ok(n) = v.parse::<u32>()
|
||||
{
|
||||
cfg.ksf_memory_kib = n;
|
||||
}
|
||||
if let Ok(v) = env::var("OXICLOUD_OPAQUE_KSF_ITERATIONS")
|
||||
if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_KSF_ITERATIONS")
|
||||
&& let Ok(n) = v.parse::<u32>()
|
||||
{
|
||||
cfg.ksf_iterations = n;
|
||||
}
|
||||
if let Ok(v) = env::var("OXICLOUD_OPAQUE_KSF_PARALLELISM")
|
||||
if let Ok(v) = env::var("OXICLOUD_AUTH_OPAQUE_KSF_PARALLELISM")
|
||||
&& let Ok(n) = v.parse::<u32>()
|
||||
{
|
||||
cfg.ksf_parallelism = n;
|
||||
@@ -1764,7 +1764,7 @@ impl OpaqueConfig {
|
||||
/// OIDC-only or magic-link-only (`OXICLOUD_AUTH_METHODS=oidc` or
|
||||
/// `=magic_link`) has no password path for OPAQUE to shadow; any
|
||||
/// non-`Off` mode would be a no-op that still nagged them for
|
||||
/// `OXICLOUD_OPAQUE_SERVER_SETUP` at boot.
|
||||
/// `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` at boot.
|
||||
///
|
||||
/// This helper resolves the misconfig quietly: if password isn't in
|
||||
/// the allowlist AND OPAQUE mode is non-`Off`, we downgrade to `Off`
|
||||
@@ -1787,7 +1787,7 @@ impl OpaqueConfig {
|
||||
event = "opaque.mode_downgraded",
|
||||
reason = "password_auth_disabled",
|
||||
configured_mode = ?self.mode,
|
||||
"OXICLOUD_OPAQUE_MODE is configured but password auth is disabled \
|
||||
"OXICLOUD_AUTH_OPAQUE_MODE is configured but password auth is disabled \
|
||||
via OXICLOUD_AUTH_METHODS — treating OPAQUE as off. \
|
||||
OPAQUE only replaces the password login path; enable password \
|
||||
in OXICLOUD_AUTH_METHODS to make this setting take effect."
|
||||
@@ -2490,7 +2490,7 @@ pub struct AppConfig {
|
||||
pub auth: AuthConfig,
|
||||
/// OPAQUE (RFC 9807) zero-knowledge password auth configuration.
|
||||
/// Substrate only in Phase 0 — endpoints are inert until
|
||||
/// `OXICLOUD_OPAQUE_MODE != off`.
|
||||
/// `OXICLOUD_AUTH_OPAQUE_MODE != off`.
|
||||
pub opaque: OpaqueConfig,
|
||||
/// Feature configuration
|
||||
pub features: FeaturesConfig,
|
||||
@@ -4332,7 +4332,7 @@ mod tests {
|
||||
//
|
||||
// OPAQUE is fundamentally a password mechanism; enabling its mode when
|
||||
// password auth is disabled would be a no-op that still nagged
|
||||
// operators for `OXICLOUD_OPAQUE_SERVER_SETUP` at boot. The
|
||||
// operators for `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` at boot. The
|
||||
// `effective_mode` helper resolves that quietly by downgrading to
|
||||
// Off + emitting an audit log, and these tests pin the truth table.
|
||||
|
||||
|
||||
+6
-6
@@ -1936,7 +1936,7 @@ impl AppServiceFactory {
|
||||
// `OXICLOUD_AUTH_METHODS` (password must be enabled for OPAQUE to
|
||||
// have anything to shadow), so OIDC-only / magic-link-only
|
||||
// deployments transparently get `opaque_service = None` even if
|
||||
// the operator accidentally set `OXICLOUD_OPAQUE_MODE=migrate`.
|
||||
// the operator accidentally set `OXICLOUD_AUTH_OPAQUE_MODE=migrate`.
|
||||
//
|
||||
// Failing here (missing SERVER_SETUP, malformed base64, ciphersuite
|
||||
// drift) refuses server boot — same fail-closed posture as the
|
||||
@@ -1957,9 +1957,9 @@ impl AppServiceFactory {
|
||||
DomainError::internal_error(
|
||||
"OpaqueInit",
|
||||
format!(
|
||||
"OXICLOUD_OPAQUE_MODE={:?} but the OPAQUE service failed: {}. \
|
||||
Persist a valid OXICLOUD_OPAQUE_SERVER_SETUP or set \
|
||||
OXICLOUD_OPAQUE_MODE=off. Refusing to start.",
|
||||
"OXICLOUD_AUTH_OPAQUE_MODE={:?} but the OPAQUE service failed: {}. \
|
||||
Persist a valid OXICLOUD_AUTH_OPAQUE_SERVER_SETUP or set \
|
||||
OXICLOUD_AUTH_OPAQUE_MODE=off. Refusing to start.",
|
||||
effective, e
|
||||
),
|
||||
)
|
||||
@@ -2850,9 +2850,9 @@ pub struct AppState {
|
||||
pub auth_service: Option<AuthServices>,
|
||||
/// OPAQUE aPAKE substrate (RFC 9807). Populated only when
|
||||
/// [`OpaqueConfig::effective_mode`] is not `Off` — that method
|
||||
/// cross-checks `OXICLOUD_OPAQUE_MODE` against
|
||||
/// cross-checks `OXICLOUD_AUTH_OPAQUE_MODE` against
|
||||
/// `OXICLOUD_AUTH_METHODS` so an OIDC-only or magic-link-only
|
||||
/// deployment gets `None` here even if `OXICLOUD_OPAQUE_MODE` was
|
||||
/// deployment gets `None` here even if `OXICLOUD_AUTH_OPAQUE_MODE` was
|
||||
/// set (with an audit-channel INFO explaining why). `None` also
|
||||
/// means the future OPAQUE endpoints must 404 — a handler that
|
||||
/// unwraps this without a nil check would break the phase gate.
|
||||
|
||||
@@ -387,7 +387,7 @@ mod integration_tests {
|
||||
.await;
|
||||
|
||||
// Fresh server setup for this test only — mirrors what the
|
||||
// DI factory would load from OXICLOUD_OPAQUE_SERVER_SETUP.
|
||||
// DI factory would load from OXICLOUD_AUTH_OPAQUE_SERVER_SETUP.
|
||||
let mut server_rng = OsRng;
|
||||
let server_setup = ServerSetup::<OxiCloudSuite>::new(&mut server_rng);
|
||||
|
||||
|
||||
@@ -72,10 +72,10 @@ pub struct OpaqueService {
|
||||
|
||||
impl OpaqueService {
|
||||
/// Build the service from runtime config. Expects the operator to have
|
||||
/// persisted the server setup already (via `OXICLOUD_OPAQUE_SERVER_SETUP`);
|
||||
/// persisted the server setup already (via `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP`);
|
||||
/// call [`OpaqueService::generate_server_setup_b64`] first-time and print
|
||||
/// the value for the operator to paste into their env before enabling
|
||||
/// `OXICLOUD_OPAQUE_MODE`.
|
||||
/// `OXICLOUD_AUTH_OPAQUE_MODE`.
|
||||
///
|
||||
/// Rejects with `InternalError` if the setup is missing / malformed, or
|
||||
/// with `AccessDenied` if the mode is `off` (guarding against
|
||||
@@ -84,14 +84,14 @@ impl OpaqueService {
|
||||
if config.mode == OpaqueMode::Off {
|
||||
return Err(DomainError::access_denied(
|
||||
"opaque",
|
||||
"OPAQUE is disabled (OXICLOUD_OPAQUE_MODE=off)",
|
||||
"OPAQUE is disabled (OXICLOUD_AUTH_OPAQUE_MODE=off)",
|
||||
));
|
||||
}
|
||||
let setup_b64 = config.server_setup_b64.as_deref().ok_or_else(|| {
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"opaque",
|
||||
"OXICLOUD_OPAQUE_SERVER_SETUP is required when OPAQUE is enabled — \
|
||||
"OXICLOUD_AUTH_OPAQUE_SERVER_SETUP is required when OPAQUE is enabled — \
|
||||
generate one with `oxicloud opaque-setup` and persist it in the env",
|
||||
)
|
||||
})?;
|
||||
@@ -141,7 +141,7 @@ impl OpaqueService {
|
||||
|
||||
/// Generate a fresh server setup and return it as base64. Called once
|
||||
/// per deployment; the returned string must be persisted in
|
||||
/// `OXICLOUD_OPAQUE_SERVER_SETUP` and NEVER rotated (rotating
|
||||
/// `OXICLOUD_AUTH_OPAQUE_SERVER_SETUP` and NEVER rotated (rotating
|
||||
/// invalidates every existing envelope — see
|
||||
/// `docs/plan/opaque.md` §Phase 0).
|
||||
pub fn generate_server_setup_b64() -> String {
|
||||
@@ -160,7 +160,7 @@ fn decode_server_setup(b64: &str) -> Result<ServerSetup<OxiCloudSuite>, DomainEr
|
||||
DomainError::new(
|
||||
ErrorKind::InternalError,
|
||||
"opaque",
|
||||
format!("OXICLOUD_OPAQUE_SERVER_SETUP is not valid base64: {e}"),
|
||||
format!("OXICLOUD_AUTH_OPAQUE_SERVER_SETUP is not valid base64: {e}"),
|
||||
)
|
||||
})?;
|
||||
ServerSetup::<OxiCloudSuite>::deserialize(&bytes).map_err(|e| {
|
||||
@@ -168,7 +168,7 @@ fn decode_server_setup(b64: &str) -> Result<ServerSetup<OxiCloudSuite>, DomainEr
|
||||
ErrorKind::InternalError,
|
||||
"opaque",
|
||||
format!(
|
||||
"OXICLOUD_OPAQUE_SERVER_SETUP payload does not match ciphersuite v1: {e}. \
|
||||
"OXICLOUD_AUTH_OPAQUE_SERVER_SETUP payload does not match ciphersuite v1: {e}. \
|
||||
If you rotated the ciphersuite, every user must re-register."
|
||||
),
|
||||
)
|
||||
@@ -256,7 +256,7 @@ mod tests {
|
||||
};
|
||||
let err = OpaqueService::from_config(cfg).expect_err("must reject missing setup");
|
||||
assert_eq!(err.kind, ErrorKind::InternalError);
|
||||
assert!(err.to_string().contains("OXICLOUD_OPAQUE_SERVER_SETUP"));
|
||||
assert!(err.to_string().contains("OXICLOUD_AUTH_OPAQUE_SERVER_SETUP"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user