diff --git a/Cargo.lock b/Cargo.lock index 8b2048aa2..31d5b2a45 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -141,6 +141,7 @@ dependencies = [ "arc-util", "arc-workflows", "axum", + "base64", "clap", "dirs", "http-body-util", diff --git a/apps/arc-web/app/api-client.ts b/apps/arc-web/app/api-client.ts index 3808c99d5..eab22c95b 100644 --- a/apps/arc-web/app/api-client.ts +++ b/apps/arc-web/app/api-client.ts @@ -3,6 +3,11 @@ import { getAppConfig } from "./lib/config.server"; const ARC_JWT_PRIVATE_KEY = process.env.ARC_JWT_PRIVATE_KEY; +function decodePemEnv(value: string): string { + if (value.startsWith("-----")) return value; + return Buffer.from(value, "base64").toString("utf-8"); +} + let cachedKey: CryptoKey | null = null; async function getSigningKey(): Promise { @@ -10,7 +15,7 @@ async function getSigningKey(): Promise { if (!ARC_JWT_PRIVATE_KEY) { throw new Error("ARC_JWT_PRIVATE_KEY environment variable is not set"); } - cachedKey = await importPKCS8(ARC_JWT_PRIVATE_KEY, "EdDSA"); + cachedKey = await importPKCS8(decodePemEnv(ARC_JWT_PRIVATE_KEY), "EdDSA"); return cachedKey; } diff --git a/crates/arc-api/Cargo.toml b/crates/arc-api/Cargo.toml index b8f3b2b6d..855681795 100644 --- a/crates/arc-api/Cargo.toml +++ b/crates/arc-api/Cargo.toml @@ -20,6 +20,7 @@ dirs.workspace = true sqlx.workspace = true tower = "0.5" tokio-stream = { workspace = true, features = ["sync"] } +base64.workspace = true jsonwebtoken.workspace = true tokio.workspace = true serde.workspace = true diff --git a/crates/arc-api/src/jwt_auth.rs b/crates/arc-api/src/jwt_auth.rs index 7e891c21f..3f7b52bca 100644 --- a/crates/arc-api/src/jwt_auth.rs +++ b/crates/arc-api/src/jwt_auth.rs @@ -26,6 +26,16 @@ pub enum AuthMode { Disabled, } +/// Decode a PEM env var that may be raw PEM or base64-encoded PEM. +fn decode_pem_env(name: &str, value: &str) -> String { + if value.starts_with("-----") { + return value.to_string(); + } + let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, value) + .unwrap_or_else(|e| panic!("{name} is not valid PEM or base64: {e}")); + String::from_utf8(bytes).unwrap_or_else(|e| panic!("{name} base64 decoded to invalid UTF-8: {e}")) +} + /// Resolve the authentication mode from the API config section. /// /// Call this once at startup before serving requests. Panics if the @@ -39,13 +49,15 @@ pub fn resolve_auth_mode(api_config: &crate::server_config::ApiConfig) -> AuthMo AuthMode::Disabled } ApiAuthenticationStrategy::Jwt => { - let pem = std::env::var("ARC_JWT_PUBLIC_KEY").unwrap_or_else(|_| { + let raw = std::env::var("ARC_JWT_PUBLIC_KEY").unwrap_or_else(|_| { panic!( "ARC_JWT_PUBLIC_KEY is not set. Either provide an Ed25519 public key in PEM \ - format or set authentication_strategy = \"insecure_disabled\" in \ - ~/.arc/arc.toml to allow unauthenticated access (development only)." + format (or base64-encoded PEM) or set authentication_strategy = \ + \"insecure_disabled\" in ~/.arc/arc.toml to allow unauthenticated access \ + (development only)." ) }); + let pem = decode_pem_env("ARC_JWT_PUBLIC_KEY", &raw); let key = DecodingKey::from_ed_pem(pem.as_bytes()) .expect("ARC_JWT_PUBLIC_KEY contains an invalid Ed25519 PEM public key"); AuthMode::Jwt(Arc::new(key))