From 0c8e567888228a509b9489d3a821055b4b78a473 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Sun, 28 Jun 2026 18:35:12 +0200 Subject: [PATCH] Fix OIDC: `ECDSA` private key support for `SEC1` format. --- CHANGELOG.md | 1 + crates/common/src/auth/oauth/config.rs | 9 ++- crates/common/src/config/mod.rs | 105 ++++++++++++++++++++++++- 3 files changed, 111 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 61c6c49d..a387cc64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If ## Fixed - DANE: Treat DNSSEC `bogus` as a temporary failures to prevent downgrade attacks. +- OIDC: `ECDSA` private key support for `SEC1` format. ## [0.16.11] - 2026-06-25 diff --git a/crates/common/src/auth/oauth/config.rs b/crates/common/src/auth/oauth/config.rs index 5796252c..be826968 100644 --- a/crates/common/src/auth/oauth/config.rs +++ b/crates/common/src/auth/oauth/config.rs @@ -5,7 +5,7 @@ */ use crate::{ - config::{build_ecdsa_pem, build_rsa_keypair}, + config::{EcKeyCurve, build_ecdsa_pem, build_rsa_keypair}, manager::application::Resource, }; use biscuit::{ @@ -197,19 +197,22 @@ async fn parse_ecdsa_key( auth: &OidcProvider, oidc_signature_algorithm: SignatureAlgorithm, ) -> Result<(Secret, AlgorithmParameters), String> { - let (alg, curve) = match oidc_signature_algorithm { + let (alg, curve, ec_curve) = match oidc_signature_algorithm { SignatureAlgorithm::ES256 => ( &signature::ECDSA_P256_SHA256_FIXED_SIGNING, EllipticCurve::P256, + EcKeyCurve::P256, ), SignatureAlgorithm::ES384 => ( &signature::ECDSA_P384_SHA384_FIXED_SIGNING, EllipticCurve::P384, + EcKeyCurve::P384, ), _ => unreachable!(), }; - let ecdsa_key_pair = build_ecdsa_pem(alg, auth.signature_key.secret().await?.as_ref())?; + let ecdsa_key_pair = + build_ecdsa_pem(alg, ec_curve, auth.signature_key.secret().await?.as_ref())?; let ecdsa_public_key = ecdsa_key_pair.public_key().as_ref(); let (x, y) = match oidc_signature_algorithm { diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 0a3487d1..a22a8338 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -17,6 +17,7 @@ use crate::{ use arc_swap::ArcSwap; use groupware::GroupwareConfig; use hyper::HeaderMap; +use pkcs8::EncodePrivateKey; use ring::signature::{EcdsaKeyPair, RsaKeyPair}; use store::registry::bootstrap::Bootstrap; use telemetry::Metrics; @@ -115,8 +116,15 @@ pub fn build_rsa_keypair(pem: &str) -> Result { Err("No RSA key found in PEM".to_string()) } +#[derive(Clone, Copy)] +pub enum EcKeyCurve { + P256, + P384, +} + pub fn build_ecdsa_pem( alg: &'static ring::signature::EcdsaSigningAlgorithm, + curve: EcKeyCurve, pem: &str, ) -> Result { for item in rustls_pemfile::read_all(&mut Cursor::new(pem)) { @@ -129,9 +137,104 @@ pub fn build_ecdsa_pem( ) .map_err(|err| format!("Failed to parse PKCS8 ECDSA key: {err}")); } + rustls_pemfile::Item::Sec1Key(key) => { + let pkcs8 = curve.sec1_to_pkcs8(key.secret_sec1_der())?; + return EcdsaKeyPair::from_pkcs8( + alg, + pkcs8.as_bytes(), + &ring::rand::SystemRandom::new(), + ) + .map_err(|err| format!("Failed to parse SEC1 ECDSA key: {err}")); + } _ => continue, // Skip certificates, DH params, etc. } } - Err("No ECDSA key found in PEM".to_string()) + Err("No usable ECDSA private key found in PEM (expected PKCS8 or SEC1)".to_string()) +} + +impl EcKeyCurve { + fn sec1_to_pkcs8(self, der: &[u8]) -> Result { + match self { + EcKeyCurve::P256 => p256::SecretKey::from_sec1_der(der) + .map_err(|err| format!("Failed to parse SEC1 ECDSA key: {err}"))? + .to_pkcs8_der() + .map_err(|err| format!("Failed to convert SEC1 ECDSA key to PKCS8: {err}")), + EcKeyCurve::P384 => p384::SecretKey::from_sec1_der(der) + .map_err(|err| format!("Failed to parse SEC1 ECDSA key: {err}"))? + .to_pkcs8_der() + .map_err(|err| format!("Failed to convert SEC1 ECDSA key to PKCS8: {err}")), + } + } +} + +#[cfg(test)] +mod tests { + use super::{EcKeyCurve, build_ecdsa_pem}; + use ring::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, ECDSA_P384_SHA384_FIXED_SIGNING}; + + const P256_SEC1: &str = "-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJ9a6n/cu7XaQez5ZX8z8jDFkkfsMB1P9Vbqzbaes2zOoAoGCCqGSM49 +AwEHoUQDQgAEPCbID7bo+8Nk1vIsTFhVKwRWvb9GWTzzwS75Dd8iZuFl23Twn6Sp +V2ZO1FC0WyXxcVOMZN2sJFlCjtaQS+p5Zg== +-----END EC PRIVATE KEY-----"; + + const P256_PKCS8: &str = "-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgn1rqf9y7tdpB7Pll +fzPyMMWSR+wwHU/1VurNtp6zbM6hRANCAAQ8JsgPtuj7w2TW8ixMWFUrBFa9v0ZZ +PPPBLvkN3yJm4WXbdPCfpKlXZk7UULRbJfFxU4xk3awkWUKO1pBL6nlm +-----END PRIVATE KEY-----"; + + const P384_SEC1: &str = "-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDAeecJf8ju/70Nf5nbI4DeRo/+Z3VWXUvB+GwuUczew7fyMbyc6B3EE +BskOIqvqu6egBwYFK4EEACKhZANiAAQQjDW03Xn2h9ZmmCMRx+uRaLLfg4o2XITE +pwACH9EY4IjTe9LNNp5CTjERd+RlpWxkYopmDS5Trzycz9sDxxSzzXmq90vomJqt +fTnNHPFHuR2SAiwuzUf26rcPwa7DCWk= +-----END EC PRIVATE KEY-----"; + + const P384_PKCS8: &str = "-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAeecJf8ju/70Nf5nbI +4DeRo/+Z3VWXUvB+GwuUczew7fyMbyc6B3EEBskOIqvqu6ehZANiAAQQjDW03Xn2 +h9ZmmCMRx+uRaLLfg4o2XITEpwACH9EY4IjTe9LNNp5CTjERd+RlpWxkYopmDS5T +rzycz9sDxxSzzXmq90vomJqtfTnNHPFHuR2SAiwuzUf26rcPwa7DCWk= +-----END PRIVATE KEY-----"; + + #[test] + fn ecdsa_pem_accepts_sec1_and_pkcs8() { + build_ecdsa_pem( + &ECDSA_P256_SHA256_FIXED_SIGNING, + EcKeyCurve::P256, + P256_SEC1, + ) + .expect("P-256 SEC1 key should parse"); + build_ecdsa_pem( + &ECDSA_P256_SHA256_FIXED_SIGNING, + EcKeyCurve::P256, + P256_PKCS8, + ) + .expect("P-256 PKCS8 key should parse"); + build_ecdsa_pem( + &ECDSA_P384_SHA384_FIXED_SIGNING, + EcKeyCurve::P384, + P384_SEC1, + ) + .expect("P-384 SEC1 key should parse"); + build_ecdsa_pem( + &ECDSA_P384_SHA384_FIXED_SIGNING, + EcKeyCurve::P384, + P384_PKCS8, + ) + .expect("P-384 PKCS8 key should parse"); + } + + #[test] + fn ecdsa_pem_rejects_keyless_pem() { + let err = build_ecdsa_pem( + &ECDSA_P256_SHA256_FIXED_SIGNING, + EcKeyCurve::P256, + "-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----", + ) + .unwrap_err(); + assert!(err.contains("No usable ECDSA private key"), "{err}"); + } }