From e1d50b818ea12926fefe91546895e7d0bbea2c3d Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 31 Aug 2022 18:25:20 +0200 Subject: [PATCH] Add a dedicated keystore crate --- Cargo.lock | 114 ++- crates/axum-utils/src/client_authorization.rs | 2 +- crates/config/Cargo.toml | 5 +- crates/config/src/sections/secrets.rs | 184 ++--- crates/handlers/Cargo.toml | 7 +- crates/handlers/src/lib.rs | 12 +- crates/handlers/src/oauth2/discovery.rs | 13 +- crates/handlers/src/oauth2/keys.rs | 12 +- crates/handlers/src/oauth2/token.rs | 38 +- crates/handlers/src/oauth2/userinfo.rs | 26 +- crates/jose/src/constraints.rs | 2 +- crates/jose/src/jwk/mod.rs | 95 ++- crates/jose/src/jwt/mod.rs | 178 +---- crates/jose/src/keystore/mod.rs | 21 - crates/jose/src/keystore/static_keystore.rs | 413 ----------- crates/jose/src/keystore/traits.rs | 40 -- crates/jose/src/lib.rs | 10 +- crates/jose/src/signer.rs | 72 ++ crates/jose/src/verifier.rs | 72 ++ crates/jose/tests/jws.rs | 6 +- crates/keystore/Cargo.toml | 27 + crates/keystore/src/lib.rs | 652 ++++++++++++++++++ crates/keystore/tests/generate.sh | 30 + crates/keystore/tests/keys/ec-k256.pkcs8.der | Bin 0 -> 135 bytes .../tests/keys/ec-k256.pkcs8.encrypted.der | Bin 0 -> 239 bytes .../tests/keys/ec-k256.pkcs8.encrypted.pem | 7 + crates/keystore/tests/keys/ec-k256.pkcs8.pem | 5 + crates/keystore/tests/keys/ec-k256.sec1.der | Bin 0 -> 118 bytes crates/keystore/tests/keys/ec-k256.sec1.pem | 5 + crates/keystore/tests/keys/ec-p256.pkcs8.der | Bin 0 -> 138 bytes .../tests/keys/ec-p256.pkcs8.encrypted.der | Bin 0 -> 239 bytes .../tests/keys/ec-p256.pkcs8.encrypted.pem | 7 + crates/keystore/tests/keys/ec-p256.pkcs8.pem | 5 + crates/keystore/tests/keys/ec-p256.sec1.der | Bin 0 -> 121 bytes crates/keystore/tests/keys/ec-p256.sec1.pem | 5 + crates/keystore/tests/keys/ec-p384.pkcs8.der | Bin 0 -> 185 bytes .../tests/keys/ec-p384.pkcs8.encrypted.der | Bin 0 -> 288 bytes .../tests/keys/ec-p384.pkcs8.encrypted.pem | 8 + crates/keystore/tests/keys/ec-p384.pkcs8.pem | 6 + crates/keystore/tests/keys/ec-p384.sec1.der | Bin 0 -> 167 bytes crates/keystore/tests/keys/ec-p384.sec1.pem | 6 + .../tests/keys/ec256.pkcs8.encrypted.pem | 7 + crates/keystore/tests/keys/rsa.pkcs1.der | Bin 0 -> 1191 bytes crates/keystore/tests/keys/rsa.pkcs1.pem | 27 + crates/keystore/tests/keys/rsa.pkcs8.der | Bin 0 -> 1217 bytes .../tests/keys/rsa.pkcs8.encrypted.der | Bin 0 -> 1329 bytes .../tests/keys/rsa.pkcs8.encrypted.pem | 30 + crates/keystore/tests/keys/rsa.pkcs8.pem | 28 + crates/keystore/tests/load.rs | 97 +++ 49 files changed, 1455 insertions(+), 819 deletions(-) delete mode 100644 crates/jose/src/keystore/mod.rs delete mode 100644 crates/jose/src/keystore/static_keystore.rs delete mode 100644 crates/jose/src/keystore/traits.rs create mode 100644 crates/keystore/Cargo.toml create mode 100644 crates/keystore/src/lib.rs create mode 100644 crates/keystore/tests/generate.sh create mode 100644 crates/keystore/tests/keys/ec-k256.pkcs8.der create mode 100644 crates/keystore/tests/keys/ec-k256.pkcs8.encrypted.der create mode 100644 crates/keystore/tests/keys/ec-k256.pkcs8.encrypted.pem create mode 100644 crates/keystore/tests/keys/ec-k256.pkcs8.pem create mode 100644 crates/keystore/tests/keys/ec-k256.sec1.der create mode 100644 crates/keystore/tests/keys/ec-k256.sec1.pem create mode 100644 crates/keystore/tests/keys/ec-p256.pkcs8.der create mode 100644 crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.der create mode 100644 crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.pem create mode 100644 crates/keystore/tests/keys/ec-p256.pkcs8.pem create mode 100644 crates/keystore/tests/keys/ec-p256.sec1.der create mode 100644 crates/keystore/tests/keys/ec-p256.sec1.pem create mode 100644 crates/keystore/tests/keys/ec-p384.pkcs8.der create mode 100644 crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.der create mode 100644 crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.pem create mode 100644 crates/keystore/tests/keys/ec-p384.pkcs8.pem create mode 100644 crates/keystore/tests/keys/ec-p384.sec1.der create mode 100644 crates/keystore/tests/keys/ec-p384.sec1.pem create mode 100644 crates/keystore/tests/keys/ec256.pkcs8.encrypted.pem create mode 100644 crates/keystore/tests/keys/rsa.pkcs1.der create mode 100644 crates/keystore/tests/keys/rsa.pkcs1.pem create mode 100644 crates/keystore/tests/keys/rsa.pkcs8.der create mode 100644 crates/keystore/tests/keys/rsa.pkcs8.encrypted.der create mode 100644 crates/keystore/tests/keys/rsa.pkcs8.encrypted.pem create mode 100644 crates/keystore/tests/keys/rsa.pkcs8.pem create mode 100644 crates/keystore/tests/load.rs diff --git a/Cargo.lock b/Cargo.lock index 2079f8be..b83c3dd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -48,6 +48,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe0133578c0986e1fe3dfcd4af1cc5b2dd6c3dbf534d69916ce16a2701d40ba" +dependencies = [ + "cfg-if", + "cipher 0.4.3", + "cpufeatures", +] + [[package]] name = "aes-gcm" version = "0.9.4" @@ -55,7 +66,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" dependencies = [ "aead 0.4.3", - "aes", + "aes 0.7.5", "cipher 0.3.0", "ctr", "ghash", @@ -655,6 +666,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a90ec2df9600c28a01c56c4784c9207a96d2451833aeceb8cc97e4c9548bb78" +dependencies = [ + "generic-array", +] + [[package]] name = "brotli" version = "3.3.4" @@ -729,6 +749,15 @@ dependencies = [ "either", ] +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.3", +] + [[package]] name = "cc" version = "1.0.73" @@ -2083,6 +2112,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array", ] @@ -2405,10 +2435,9 @@ dependencies = [ "lettre", "mas-iana", "mas-jose", - "p256", - "pkcs8", + "mas-keystore", + "pem-rfc7468", "rand", - "rsa", "schemars", "serde", "serde_json", @@ -2461,9 +2490,7 @@ dependencies = [ "axum-extra", "axum-macros", "chrono", - "crc", "data-encoding", - "elliptic-curve", "headers", "hyper", "indoc", @@ -2475,15 +2502,14 @@ dependencies = [ "mas-http", "mas-iana", "mas-jose", + "mas-keystore", "mas-policy", "mas-router", "mas-storage", "mas-templates", "mime", "oauth2-types", - "pkcs8", "rand", - "rsa", "serde", "serde_json", "serde_urlencoded", @@ -2586,6 +2612,30 @@ dependencies = [ "url", ] +[[package]] +name = "mas-keystore" +version = "0.1.0" +dependencies = [ + "anyhow", + "const-oid", + "der", + "ecdsa", + "elliptic-curve", + "k256", + "mas-iana", + "mas-jose", + "p256", + "p384", + "pem-rfc7468", + "pkcs1", + "pkcs8", + "rand_core", + "rsa", + "sec1", + "spki", + "thiserror", +] + [[package]] name = "mas-policy" version = "0.1.0" @@ -3208,6 +3258,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "pear" version = "0.2.3" @@ -3382,6 +3441,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "pkcs5" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d10d862c1f5c302df3c3dbfd837afbae0ad09551a6fa37b10311cb5890a80175" +dependencies = [ + "aes 0.8.1", + "cbc", + "der", + "hmac", + "pbkdf2", + "scrypt", + "sha2 0.10.2", + "spki", +] + [[package]] name = "pkcs8" version = "0.9.0" @@ -3389,6 +3464,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" dependencies = [ "der", + "pkcs5", + "rand_core", "spki", ] @@ -3890,6 +3967,15 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher 0.4.3", +] + [[package]] name = "same-file" version = "1.0.6" @@ -3941,6 +4027,18 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "scrypt" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" +dependencies = [ + "hmac", + "pbkdf2", + "salsa20", + "sha2 0.10.2", +] + [[package]] name = "sct" version = "0.6.1" diff --git a/crates/axum-utils/src/client_authorization.rs b/crates/axum-utils/src/client_authorization.rs index 94abc9f2..2dbec7e7 100644 --- a/crates/axum-utils/src/client_authorization.rs +++ b/crates/axum-utils/src/client_authorization.rs @@ -30,7 +30,7 @@ use mas_config::Encrypter; use mas_data_model::{Client, JwksOrJwksUri, StorageBackend}; use mas_http::HttpServiceExt; use mas_iana::oauth::OAuthClientAuthenticationMethod; -use mas_jose::{jwk::PublicJsonWebKeySet, Jwt}; +use mas_jose::{jwk::PublicJsonWebKeySet, jwt::Jwt}; use mas_storage::{ oauth2::client::{lookup_client_by_client_id, ClientFetchError}, PostgresqlBackend, diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 6e301d6a..410ec10d 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -24,10 +24,8 @@ serde_json = "1.0.85" sqlx = { version = "0.6.1", features = ["runtime-tokio-rustls", "postgres"] } lettre = { version = "0.10.1", default-features = false, features = ["serde", "builder"] } +pem-rfc7468 = "0.6.0" rand = "0.8.5" -rsa = { git = "https://github.com/RustCrypto/RSA.git" } -p256 = { version = "0.11.1", features = ["ecdsa", "pem", "pkcs8"] } -pkcs8 = { version = "0.9.0", features = ["pem"] } chacha20poly1305 = { version = "0.10.1", features = ["std"] } cookie = { version = "0.16.0", features = ["private", "key-expansion"] } data-encoding = "2.3.2" @@ -35,4 +33,5 @@ data-encoding = "2.3.2" indoc = "1.0.7" mas-jose = { path = "../jose" } +mas-keystore = { path = "../keystore" } mas-iana = { path = "../iana" } diff --git a/crates/config/src/sections/secrets.rs b/crates/config/src/sections/secrets.rs index efc3e7cd..6feed113 100644 --- a/crates/config/src/sections/secrets.rs +++ b/crates/config/src/sections/secrets.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{path::PathBuf, sync::Arc}; +use std::{borrow::Cow, path::PathBuf, sync::Arc}; use anyhow::Context; use async_trait::async_trait; @@ -22,16 +22,16 @@ use chacha20poly1305::{ }; use cookie::Key; use data_encoding::BASE64; -use mas_jose::StaticKeystore; -use pkcs8::DecodePrivateKey; -use rsa::{ - pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey}, - RsaPrivateKey, +use mas_jose::jwk::{JsonWebKey, JsonWebKeySet}; +use mas_keystore::{Keystore, PrivateKey}; +use rand::{ + distributions::{Alphanumeric, DistString}, + thread_rng, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_with::serde_as; -use tokio::{fs::File, io::AsyncReadExt, task}; +use tokio::task; use tracing::info; use super::ConfigurationSection; @@ -124,25 +124,29 @@ fn example_secret() -> &'static str { "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff" } -#[derive(JsonSchema, Serialize, Deserialize, Clone, Copy, Debug)] -#[serde(rename_all = "lowercase")] -pub enum KeyType { - Rsa, - Ecdsa, +#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "snake_case")] +pub enum KeyOrFile { + Key(String), + KeyFile(PathBuf), } #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "snake_case")] -pub enum KeyOrPath { - Key(String), - Path(PathBuf), +pub enum PasswordOrFile { + Password(String), + PasswordFile(PathBuf), } #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)] pub struct KeyConfig { - r#type: KeyType, + kid: String, + #[serde(flatten)] - key: KeyOrPath, + password: Option, + + #[serde(flatten)] + key: KeyOrFile, } /// Application secrets @@ -169,58 +173,45 @@ impl SecretsConfig { /// # Errors /// /// Returns an error when a key could not be imported - pub async fn key_store(&self) -> anyhow::Result { - let mut store = StaticKeystore::new(); - + pub async fn key_store(&self) -> anyhow::Result { + let mut keys = Vec::with_capacity(self.keys.len()); for item in &self.keys { - // Read the key either embedded in the config file or on disk - let mut buf = Vec::new(); - let (key_as_bytes, key_as_str) = match &item.key { - KeyOrPath::Key(key) => (key.as_bytes(), Some(key.as_str())), - KeyOrPath::Path(path) => { - let mut file = File::open(path).await?; - file.read_to_end(&mut buf).await?; + let password = match &item.password { + Some(PasswordOrFile::Password(password)) => Some(Cow::Borrowed(password.as_str())), + Some(PasswordOrFile::PasswordFile(path)) => { + Some(Cow::Owned(tokio::fs::read_to_string(path).await?)) + } + None => None, + }; - (&buf[..], std::str::from_utf8(&buf).ok()) + // Read the key either embedded in the config file or on disk + let key = match &item.key { + KeyOrFile::Key(key) => { + // If the key was embedded in the config file, assume it is formatted as PEM + if let Some(password) = password { + PrivateKey::load_encrypted_pem(key, password.as_bytes())? + } else { + PrivateKey::load_pem(key)? + } + } + KeyOrFile::KeyFile(path) => { + // When reading from disk, it might be either PEM or DER. `PrivateKey::load*` + // will try both. + let key = tokio::fs::read(path).await?; + if let Some(password) = password { + PrivateKey::load_encrypted(&key, password.as_bytes())? + } else { + PrivateKey::load(&key)? + } } }; - match item.r#type { - // TODO: errors are not well carried here - KeyType::Ecdsa => { - // First try to read it as DER from the bytes - let mut key = p256::SecretKey::from_pkcs1_der(key_as_bytes) - .or_else(|_| p256::SecretKey::from_pkcs8_der(key_as_bytes)) - .or_else(|_| p256::SecretKey::from_sec1_der(key_as_bytes)); - - // If the file was a valid string, try reading it as PEM - if let Some(key_as_str) = key_as_str { - key = key - .or_else(|_| p256::SecretKey::from_pkcs1_pem(key_as_str)) - .or_else(|_| p256::SecretKey::from_pkcs8_pem(key_as_str)) - .or_else(|_| p256::SecretKey::from_sec1_pem(key_as_str)); - } - - let key = key?; - store.add_ecdsa_key(key.into())?; - } - KeyType::Rsa => { - let mut key = rsa::RsaPrivateKey::from_pkcs1_der(key_as_bytes) - .or_else(|_| rsa::RsaPrivateKey::from_pkcs8_der(key_as_bytes)); - - if let Some(key_as_str) = key_as_str { - key = key - .or_else(|_| rsa::RsaPrivateKey::from_pkcs1_pem(key_as_str)) - .or_else(|_| rsa::RsaPrivateKey::from_pkcs8_pem(key_as_str)); - } - - let key = key?; - store.add_rsa_key(key)?; - } - } + let key = JsonWebKey::new(key).with_kid(item.kid.clone()); + keys.push(key); } - Ok(store) + let keys = JsonWebKeySet::new(keys); + Ok(Keystore::new(keys)) } /// Derive an [`Encrypter`] out of the config @@ -243,44 +234,74 @@ impl ConfigurationSection<'_> for SecretsConfig { let span = tracing::info_span!("rsa"); let rsa_key = task::spawn_blocking(move || { let _entered = span.enter(); - let mut rng = rand::thread_rng(); - let ret = - RsaPrivateKey::new(&mut rng, 2048).context("could not generate RSA private key"); + let ret = PrivateKey::generate_rsa(thread_rng()).unwrap(); info!("Done generating RSA key"); ret }) .await - .context("could not join blocking task")??; + .context("could not join blocking task")?; let rsa_key = KeyConfig { - r#type: KeyType::Rsa, - key: KeyOrPath::Key(rsa_key.to_pkcs1_pem(pkcs8::LineEnding::LF)?.to_string()), + kid: Alphanumeric.sample_string(&mut thread_rng(), 10), + password: None, + key: KeyOrFile::Key(rsa_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), }; - let span = tracing::info_span!("ecdsa"); - let ecdsa_key = task::spawn_blocking(move || { + let span = tracing::info_span!("ec_p256"); + let ec_p256_key = task::spawn_blocking(move || { let _entered = span.enter(); - let rng = rand::thread_rng(); - let ret = p256::SecretKey::random(rng); - info!("Done generating ECDSA key"); + let ret = PrivateKey::generate_ec_p256(thread_rng()); + info!("Done generating EC P-256 key"); ret }) .await .context("could not join blocking task")?; - let ecdsa_key = KeyConfig { - r#type: KeyType::Ecdsa, - key: KeyOrPath::Key(ecdsa_key.to_pem(pkcs8::LineEnding::LF)?.to_string()), + let ec_p256_key = KeyConfig { + kid: Alphanumeric.sample_string(&mut thread_rng(), 10), + password: None, + key: KeyOrFile::Key(ec_p256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + }; + + let span = tracing::info_span!("ec_p384"); + let ec_p384_key = task::spawn_blocking(move || { + let _entered = span.enter(); + let ret = PrivateKey::generate_ec_p384(thread_rng()); + info!("Done generating EC P-256 key"); + ret + }) + .await + .context("could not join blocking task")?; + let ec_p384_key = KeyConfig { + kid: Alphanumeric.sample_string(&mut thread_rng(), 10), + password: None, + key: KeyOrFile::Key(ec_p384_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), + }; + + let span = tracing::info_span!("ec_k256"); + let ec_k256_key = task::spawn_blocking(move || { + let _entered = span.enter(); + let ret = PrivateKey::generate_ec_k256(thread_rng()); + info!("Done generating EC secp256k1 key"); + ret + }) + .await + .context("could not join blocking task")?; + let ec_k256_key = KeyConfig { + kid: Alphanumeric.sample_string(&mut thread_rng(), 10), + password: None, + key: KeyOrFile::Key(ec_k256_key.to_pem(pem_rfc7468::LineEnding::LF)?.to_string()), }; Ok(Self { encryption: rand::random(), - keys: vec![rsa_key, ecdsa_key], + keys: vec![rsa_key, ec_p256_key, ec_p384_key, ec_k256_key], }) } fn test() -> Self { let rsa_key = KeyConfig { - r#type: KeyType::Rsa, - key: KeyOrPath::Key( + kid: "abcdef".to_owned(), + password: None, + key: KeyOrFile::Key( indoc::indoc! {r#" -----BEGIN PRIVATE KEY----- MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN @@ -297,8 +318,9 @@ impl ConfigurationSection<'_> for SecretsConfig { ), }; let ecdsa_key = KeyConfig { - r#type: KeyType::Ecdsa, - key: KeyOrPath::Key( + kid: "ghijkl".to_owned(), + password: None, + key: KeyOrFile::Key( indoc::indoc! {r#" -----BEGIN PRIVATE KEY----- MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA diff --git a/crates/handlers/Cargo.toml b/crates/handlers/Cargo.toml index 69a49812..54a78273 100644 --- a/crates/handlers/Cargo.toml +++ b/crates/handlers/Cargo.toml @@ -40,11 +40,7 @@ serde_urlencoded = "0.7.1" argon2 = { version = "0.4.1", features = ["password-hash"] } # Crypto, hashing and signing stuff -rsa = { git = "https://github.com/RustCrypto/RSA.git" } -pkcs8 = { version = "0.9.0", features = ["pem"] } -elliptic-curve = { version = "0.12.3", features = ["pem"] } sha2 = "0.10.2" -crc = "3.0.0" # Various data types and utilities data-encoding = "2.3.2" @@ -62,10 +58,11 @@ mas-email = { path = "../email" } mas-http = { path = "../http" } mas-iana = { path = "../iana" } mas-jose = { path = "../jose" } +mas-keystore = { path = "../keystore" } mas-policy = { path = "../policy" } +mas-router = { path = "../router" } mas-storage = { path = "../storage" } mas-templates = { path = "../templates" } -mas-router = { path = "../router" } [dev-dependencies] indoc = "1.0.7" diff --git a/crates/handlers/src/lib.rs b/crates/handlers/src/lib.rs index 64bf330c..d5baf2bf 100644 --- a/crates/handlers/src/lib.rs +++ b/crates/handlers/src/lib.rs @@ -33,7 +33,7 @@ use hyper::header::{ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_LANGUAGE, CO use mas_config::{Encrypter, MatrixConfig}; use mas_email::Mailer; use mas_http::CorsLayerExt; -use mas_jose::StaticKeystore; +use mas_keystore::Keystore; use mas_policy::PolicyFactory; use mas_router::{Route, UrlBuilder}; use mas_templates::{ErrorContext, Templates}; @@ -56,7 +56,7 @@ mod views; pub fn router( pool: &PgPool, templates: &Templates, - key_store: &Arc, + key_store: &Keystore, encrypter: &Encrypter, mailer: &Mailer, url_builder: &UrlBuilder, @@ -251,12 +251,8 @@ async fn test_router(pool: &PgPool) -> Result { let templates_config = TemplatesConfig::default(); let templates = Templates::load_from_config(&templates_config).await?; - let key_store = { - let mut k = StaticKeystore::new(); - k.add_test_rsa_key()?; - k.add_test_ecdsa_key()?; - Arc::new(k) - }; + // TODO: add test keys to the store + let key_store = Keystore::default(); let encrypter = Encrypter::new(&[0x42; 32]); diff --git a/crates/handlers/src/oauth2/discovery.rs b/crates/handlers/src/oauth2/discovery.rs index b78db5dc..0d333de8 100644 --- a/crates/handlers/src/oauth2/discovery.rs +++ b/crates/handlers/src/oauth2/discovery.rs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; - use axum::{extract::Extension, response::IntoResponse, Json}; use mas_iana::{ jose::JsonWebSignatureAlg, @@ -22,7 +20,7 @@ use mas_iana::{ PkceCodeChallengeMethod, }, }; -use mas_jose::{SigningKeystore, StaticKeystore}; +use mas_keystore::Keystore; use mas_router::UrlBuilder; use oauth2_types::{ oidc::{ClaimType, ProviderMetadata, SubjectType}, @@ -32,7 +30,7 @@ use oauth2_types::{ #[allow(clippy::too_many_lines)] pub(crate) async fn get( - Extension(key_store): Extension>, + Extension(key_store): Extension, Extension(url_builder): Extension, ) -> impl IntoResponse { // This is how clients can authenticate @@ -54,12 +52,7 @@ pub(crate) async fn get( ]); // This is how we can sign stuff - let jwt_signing_alg_values_supported = Some({ - let algs = key_store.supported_algorithms(); - let mut algs = Vec::from_iter(algs); - algs.sort(); - algs - }); + let jwt_signing_alg_values_supported = Some(key_store.available_signing_algorithms()); // Prepare all the endpoints let issuer = Some(url_builder.oidc_issuer()); diff --git a/crates/handlers/src/oauth2/keys.rs b/crates/handlers/src/oauth2/keys.rs index a883eddc..3c766786 100644 --- a/crates/handlers/src/oauth2/keys.rs +++ b/crates/handlers/src/oauth2/keys.rs @@ -12,14 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{convert::Infallible, sync::Arc}; - use axum::{extract::Extension, response::IntoResponse, Json}; -use mas_jose::StaticKeystore; +use mas_keystore::Keystore; -pub(crate) async fn get( - Extension(key_store): Extension>, -) -> Result { - let jwks = key_store.to_public_jwks(); - Ok(Json(jwks)) +pub(crate) async fn get(Extension(key_store): Extension) -> impl IntoResponse { + let jwks = key_store.public_jwks(); + Json(jwks) } diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index 478f9a4f..d8c7f45e 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; use anyhow::Context; use axum::{extract::Extension, response::IntoResponse, Json}; @@ -26,8 +26,10 @@ use mas_data_model::{AuthorizationGrantStage, Client, TokenType}; use mas_iana::jose::JsonWebSignatureAlg; use mas_jose::{ claims::{self, ClaimError}, - DecodedJsonWebToken, JwtSignatureError, SigningKeystore, StaticKeystore, + constraints::Constrainable, + jwt::{JsonWebSignatureHeader, Jwt, JwtSignatureError}, }; +use mas_keystore::Keystore; use mas_router::UrlBuilder; use mas_storage::{ oauth2::{ @@ -161,6 +163,12 @@ impl IntoResponse for RouteError { } } +impl From for RouteError { + fn from(e: mas_keystore::WrongAlgorithmError) -> Self { + Self::Internal(Box::new(e)) + } +} + impl From for RouteError { fn from(e: sqlx::Error) -> Self { Self::Internal(Box::new(e)) @@ -182,7 +190,7 @@ impl From for RouteError { #[tracing::instrument(skip_all, err)] pub(crate) async fn post( client_authorization: ClientAuthorization, - Extension(key_store): Extension>, + Extension(key_store): Extension, Extension(url_builder): Extension, Extension(pool): Extension, Extension(encrypter): Extension, @@ -235,7 +243,7 @@ fn hash(mut hasher: H, token: &str) -> anyhow::Result { async fn authorization_code_grant( grant: &AuthorizationCodeGrant, client: &Client, - key_store: &StaticKeystore, + key_store: &Keystore, url_builder: &UrlBuilder, mut txn: Transaction<'_, Postgres>, ) -> Result { @@ -339,17 +347,19 @@ async fn authorization_code_grant( claims::AT_HASH.insert(&mut claims, hash(Sha256::new(), &access_token_str)?)?; claims::C_HASH.insert(&mut claims, hash(Sha256::new(), &grant.code)?)?; - let header = key_store - .prepare_header( - client - .id_token_signed_response_alg - .unwrap_or(JsonWebSignatureAlg::Rs256), - ) - .await?; - let id_token = DecodedJsonWebToken::new(header, claims); - let id_token = id_token.sign(key_store).await?; + let alg = client + .id_token_signed_response_alg + .unwrap_or(JsonWebSignatureAlg::Rs256); + let key = key_store + .signing_key_for_algorithm(alg) + .context("no suitable key found")?; - Some(id_token.serialize()) + let header = JsonWebSignatureHeader::new(alg) + .with_kid(key.kid().context("key has no `kid` for some reason")?); + let signer = key.params().signer_for_alg(alg)?; + let id_token = Jwt::sign(header, claims, &signer)?; + + Some(id_token.as_str().to_owned()) } else { None }; diff --git a/crates/handlers/src/oauth2/userinfo.rs b/crates/handlers/src/oauth2/userinfo.rs index edf8b2a8..9132ef18 100644 --- a/crates/handlers/src/oauth2/userinfo.rs +++ b/crates/handlers/src/oauth2/userinfo.rs @@ -12,8 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::sync::Arc; - +use anyhow::Context; use axum::{ extract::Extension, response::{IntoResponse, Response}, @@ -21,7 +20,11 @@ use axum::{ }; use headers::ContentType; use mas_axum_utils::{user_authorization::UserAuthorization, FancyError}; -use mas_jose::{DecodedJsonWebToken, SigningKeystore, StaticKeystore}; +use mas_jose::{ + constraints::Constrainable, + jwt::{JsonWebSignatureHeader, Jwt}, +}; +use mas_keystore::Keystore; use mas_router::UrlBuilder; use mime::Mime; use oauth2_types::scope; @@ -49,7 +52,7 @@ struct SignedUserInfo { pub async fn get( Extension(url_builder): Extension, Extension(pool): Extension, - Extension(key_store): Extension>, + Extension(key_store): Extension, user_authorization: UserAuthorization, ) -> Result { // TODO: error handling @@ -73,7 +76,13 @@ pub async fn get( } if let Some(alg) = session.client.userinfo_signed_response_alg { - let header = key_store.prepare_header(alg).await?; + let key = key_store + .signing_key_for_algorithm(alg) + .context("no suitable key found")?; + + let header = JsonWebSignatureHeader::new(alg) + .with_kid(key.kid().context("key has no `kid` for some reason")?); + let signer = key.params().signer_for_alg(alg)?; let user_info = SignedUserInfo { iss: url_builder.oidc_issuer().to_string(), @@ -81,13 +90,10 @@ pub async fn get( user_info, }; - let user_info = DecodedJsonWebToken::new(header, user_info); - let user_info = user_info.sign(key_store.as_ref()).await?; - - let token = user_info.serialize(); + let token = Jwt::sign(header, user_info, &signer)?; let application_jwt: Mime = "application/jwt".parse().unwrap(); let content_type = ContentType::from(application_jwt); - Ok((TypedHeader(content_type), token).into_response()) + Ok((TypedHeader(content_type), token.as_str().to_owned()).into_response()) } else { Ok(Json(user_info).into_response()) } diff --git a/crates/jose/src/constraints.rs b/crates/jose/src/constraints.rs index c542185b..57373452 100644 --- a/crates/jose/src/constraints.rs +++ b/crates/jose/src/constraints.rs @@ -16,7 +16,7 @@ use std::collections::HashSet; use mas_iana::jose::{JsonWebKeyType, JsonWebKeyUse, JsonWebSignatureAlg}; -use crate::JsonWebSignatureHeader; +use crate::jwt::JsonWebSignatureHeader; #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Constraint<'a> { diff --git a/crates/jose/src/jwk/mod.rs b/crates/jose/src/jwk/mod.rs index bdcc89c3..9c570079 100644 --- a/crates/jose/src/jwk/mod.rs +++ b/crates/jose/src/jwk/mod.rs @@ -104,17 +104,7 @@ impl TryFrom for PublicJsonWebKey { type Error = SymetricKeyError; fn try_from(value: PrivateJsonWebKey) -> Result { - Ok(Self { - parameters: value.parameters.try_into()?, - r#use: value.r#use, - key_ops: value.key_ops, - alg: value.alg, - kid: value.kid, - x5u: value.x5u, - x5c: value.x5c, - x5t: value.x5t, - x5t_s256: value.x5t_s256, - }) + value.try_map(JsonWebKeyPublicParameters::try_from) } } @@ -134,6 +124,74 @@ impl

JsonWebKey

{ } } + pub fn try_map(self, mapper: M) -> Result, E> + where + M: FnOnce(P) -> Result, + { + Ok(JsonWebKey { + parameters: mapper(self.parameters)?, + r#use: self.r#use, + key_ops: self.key_ops, + alg: self.alg, + kid: self.kid, + x5u: self.x5u, + x5c: self.x5c, + x5t: self.x5t, + x5t_s256: self.x5t_s256, + }) + } + + pub fn map(self, mapper: M) -> JsonWebKey + where + M: FnOnce(P) -> O, + { + JsonWebKey { + parameters: mapper(self.parameters), + r#use: self.r#use, + key_ops: self.key_ops, + alg: self.alg, + kid: self.kid, + x5u: self.x5u, + x5c: self.x5c, + x5t: self.x5t, + x5t_s256: self.x5t_s256, + } + } + + pub fn try_cloned_map(&self, mapper: M) -> Result, E> + where + M: FnOnce(&P) -> Result, + { + Ok(JsonWebKey { + parameters: mapper(&self.parameters)?, + r#use: self.r#use, + key_ops: self.key_ops.clone(), + alg: self.alg, + kid: self.kid.clone(), + x5u: self.x5u.clone(), + x5c: self.x5c.clone(), + x5t: self.x5t.clone(), + x5t_s256: self.x5t_s256.clone(), + }) + } + + pub fn cloned_map(&self, mapper: M) -> JsonWebKey + where + M: FnOnce(&P) -> O, + { + JsonWebKey { + parameters: mapper(&self.parameters), + r#use: self.r#use, + key_ops: self.key_ops.clone(), + alg: self.alg, + kid: self.kid.clone(), + x5u: self.x5u.clone(), + x5c: self.x5c.clone(), + x5t: self.x5t.clone(), + x5t_s256: self.x5t_s256.clone(), + } + } + #[must_use] pub const fn with_use(mut self, value: JsonWebKeyUse) -> Self { self.r#use = Some(value); @@ -199,6 +257,14 @@ pub struct JsonWebKeySet

{ keys: Vec>, } +impl

Default for JsonWebKeySet

{ + fn default() -> Self { + Self { + keys: Vec::default(), + } + } +} + pub type PublicJsonWebKeySet = JsonWebKeySet; pub type PrivateJsonWebKeySet = JsonWebKeySet; @@ -229,6 +295,13 @@ impl

JsonWebKeySet

{ } } +impl

FromIterator> for JsonWebKeySet

{ + fn from_iter>>(iter: T) -> Self { + let keys = iter.into_iter().collect(); + Self { keys } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/jose/src/jwt/mod.rs b/crates/jose/src/jwt/mod.rs index 7d4abbaf..763543ba 100644 --- a/crates/jose/src/jwt/mod.rs +++ b/crates/jose/src/jwt/mod.rs @@ -12,181 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -use std::str::FromStr; - -use base64ct::{Base64UrlUnpadded, Encoding}; -use serde::{de::DeserializeOwned, Serialize}; -use thiserror::Error; - -use crate::{SigningKeystore, VerifyingKeystore}; - mod header; mod raw; mod signed; -pub use self::{header::JsonWebSignatureHeader, signed::Jwt}; - -#[derive(Debug, PartialEq, Eq)] -pub struct JsonWebTokenParts { - payload: String, - signature: Vec, -} - -#[derive(Error, Debug)] -#[error("failed to decode JWT")] -pub enum JwtPartsDecodeError { - #[error("no dots found in the JWT")] - NoDots, - - #[error("could not decode signature")] - SignatureEncoding { - #[from] - inner: base64ct::Error, - }, -} - -impl FromStr for JsonWebTokenParts { - type Err = JwtPartsDecodeError; - - fn from_str(s: &str) -> Result { - let (payload, signature) = s.rsplit_once('.').ok_or(JwtPartsDecodeError::NoDots)?; - let signature = Base64UrlUnpadded::decode_vec(signature)?; - let payload = payload.to_owned(); - Ok(Self { payload, signature }) - } -} - -#[derive(Error, Debug)] -#[error("failed to serialize JWT")] -pub enum JwtSerializeError { - #[error("failed to serialize JWT header")] - Header { - #[source] - inner: serde_json::Error, - }, - - #[error("failed to serialize payload")] - Payload { - #[source] - inner: serde_json::Error, - }, -} - -#[derive(Error, Debug)] -#[error("failed to serialize JWT")] -pub enum JwtSignatureError { - Serialize { - #[from] - inner: JwtSerializeError, - }, - - Sign { - #[source] - inner: anyhow::Error, - }, -} - -pub struct DecodedJsonWebToken { - header: JsonWebSignatureHeader, - payload: T, -} - -impl DecodedJsonWebToken -where - T: Serialize, -{ - fn serialize(&self) -> Result { - let header = serde_json::to_vec(&self.header) - .map_err(|inner| JwtSerializeError::Header { inner })?; - let header = Base64UrlUnpadded::encode_string(&header); - - let payload = serde_json::to_vec(&self.payload) - .map_err(|inner| JwtSerializeError::Payload { inner })?; - let payload = Base64UrlUnpadded::encode_string(&payload); - - Ok(format!("{}.{}", header, payload)) - } - - pub async fn sign( - &self, - store: &S, - ) -> Result { - let payload = self.serialize()?; - let signature = store - .sign(&self.header, payload.as_bytes()) - .await - .map_err(|inner| JwtSignatureError::Sign { inner })?; - Ok(JsonWebTokenParts { payload, signature }) - } -} - -impl DecodedJsonWebToken { - pub fn new(header: JsonWebSignatureHeader, payload: T) -> Self { - Self { header, payload } - } - - pub fn claims(&self) -> &T { - &self.payload - } - - pub fn header(&self) -> &JsonWebSignatureHeader { - &self.header - } - - pub fn split(self) -> (JsonWebSignatureHeader, T) { - (self.header, self.payload) - } -} - -impl FromStr for DecodedJsonWebToken -where - T: DeserializeOwned, -{ - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let (header, payload) = s - .split_once('.') - .ok_or_else(|| anyhow::anyhow!("invalid payload"))?; - - let header = Base64UrlUnpadded::decode_vec(header)?; - let header = serde_json::from_slice(&header)?; - let payload = Base64UrlUnpadded::decode_vec(payload)?; - let payload = serde_json::from_slice(&payload)?; - Ok(Self { header, payload }) - } -} - -impl JsonWebTokenParts { - pub fn decode(&self) -> anyhow::Result> { - let decoded = self.payload.parse()?; - Ok(decoded) - } - - pub fn verify( - &self, - header: &JsonWebSignatureHeader, - store: &S, - ) -> S::Future { - store.verify(header, self.payload.as_bytes(), &self.signature) - } - - pub async fn decode_and_verify( - &self, - store: &S, - ) -> anyhow::Result> - where - S::Error: std::error::Error + Send + Sync + 'static, - { - let decoded = self.decode()?; - self.verify(&decoded.header, store).await?; - Ok(decoded) - } - - #[must_use] - pub fn serialize(&self) -> String { - let payload = &self.payload; - let signature = Base64UrlUnpadded::encode_string(&self.signature); - format!("{}.{}", payload, signature) - } -} +pub use self::{ + header::JsonWebSignatureHeader, + signed::{Jwt, JwtDecodeError, JwtSignatureError, JwtVerificationError}, +}; diff --git a/crates/jose/src/keystore/mod.rs b/crates/jose/src/keystore/mod.rs deleted file mode 100644 index 21a8c326..00000000 --- a/crates/jose/src/keystore/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2022 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -mod static_keystore; -mod traits; - -pub use self::{ - static_keystore::StaticKeystore, - traits::{SigningKeystore, VerifyingKeystore}, -}; diff --git a/crates/jose/src/keystore/static_keystore.rs b/crates/jose/src/keystore/static_keystore.rs deleted file mode 100644 index dc2f6432..00000000 --- a/crates/jose/src/keystore/static_keystore.rs +++ /dev/null @@ -1,413 +0,0 @@ -// Copyright 2022 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::{ - collections::{HashMap, HashSet}, - future::Ready, -}; - -use anyhow::bail; -use async_trait::async_trait; -use base64ct::{Base64UrlUnpadded, Encoding}; -use digest::Digest; -use ecdsa::{SigningKey, VerifyingKey}; -use mas_iana::jose::{JsonWebKeyUse, JsonWebSignatureAlg}; -use p256::{NistP256, PublicKey}; -use pkcs1::{DecodeRsaPrivateKey, EncodeRsaPublicKey}; -use pkcs8::{DecodePrivateKey, EncodePublicKey}; -use rsa::{PublicKey as _, RsaPrivateKey, RsaPublicKey}; -use sha2::{Sha256, Sha384, Sha512}; -use signature::{Signature, Signer, Verifier}; - -use super::{SigningKeystore, VerifyingKeystore}; -use crate::{ - jwk::{JsonWebKey, PublicJsonWebKeySet}, - JsonWebSignatureHeader, -}; - -// Generate with -// openssl genrsa 2048 -const TEST_RSA_PKCS1_PEM: &str = "-----BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA1j7Y2CH6Ss8tgaNvcQPaRJKnCZD8ABqNPyKDWLQLph6Zi7gZ -GqmRtTzMuevo2ezpkbCiQAPEp1ms022P92bB+uqG7xmzHTzbwLtnq3OAdjmrnaFV -I4v89WHUsTXX9hiYOK5dOM81bNZ6muxWZ0L/xw4jVWe7xkqnp2Lluq0HknlzP5yJ -UEikf5BkpX0iyIu2/X4r8YVp8uzG34l/8qBx6k3rO2VkOQOSybZj1oij5KZCusnu -QjJLKWXCqJToWE6iVn+Q0N6ySDLgmJ7Zq0Sou/9N/oWKn94FOsouQgET5NuzoIFR -qTb321fQ8gbqt/OupBbBKEo1qUU+cS77TD/AuQIDAQABAoIBAQDLSZzmD+93lnf+ -f36ZxOcRk/nNGPYUfx0xH+VzgHthJ73YFlozs1xflQ5JB/DM/4BsziZWCX1KsctM -XrRxMt6y4GAidcc/4eQ+T1RCGfl1tKkDi/bGIOloSGjRsV5208V0WvZ3lh2CZUy2 -vbQKjUc3sFGUkzZYI7RLHosPA2mg78IVuSnqvNaU0TgA2KkaxWs6Ecr/ys80cUvj -KKj04DmX5xaXwUKmz353i5gIt3aY3G5CAw5fU/ocDKR8nzVCpBAGbRRiUaVKIT06 -APSkLDTUnxSYtHtDJGHjgU/TsvAwTA92J3ue5Ysu9xTE+WyHA6Rgux7RQSD/wWHr -LdRPwxPFAoGBAOytMPh/f2zKmotanjho0QNfhAUHoQUfPudYT0nnDceOsi1jYWbQ -c/wPeQQC4Hp/pTUrkSIQPEz/hSxzZ6RPxxuGB8O94I0uLwQK4V1UwbgfsRa9zQzW -n0kgKZ8w8h8B7qyiKyIAnZzvKtNEnKrzrct4HsN3OEoXTwuAUYlvWtQTAoGBAOe8 -0liNaH9V6ecZiojkRR1tiQkr/dCV+a13eXXaRA/8y/3wKCQ4idYncclQJTLKsAwW -hHuDd4uLgtifREVIBD2jGdlznNr9HQNuZgwjuUoH+r1YLGgiMWeVYSr0m8lyDlQl -BJKTAphrqo6VJWDAnM18v+by//yRleSjVMqZ3zmDAoGBAMpA0rl5EyagGON/g/hG -sl8Ej+hQdazP38yJbfCEsATaD6+z3rei6Yr8mfjwkG5+iGrgmT0XzMAsF909ndMP -jeIabqY6rBtZ3TnCJobAeG9lPctmVUVkX2h5QLhWdoJC/3iteNis2AQVam5yksOQ -S/O16ew2BHdkZds5Q/SDoYXbAoGAK9tVZ8LjWu30hXMU/9FLr0USoTS9JWOszAKH -byFuriPmq1lvD2PP2kK+yx2q3JD1fmQokIOR9Uvi6IJD1mTJwKyEcN3reppailKz -Z2q/X15hOsJcLR0DgpoHuKxwa1B1m8Ehu2etHxGJRtC9MTFiu5T3cIrenXskBhBP -NMSoNWcCgYAD3u3zdeVo3gVoxneS7GNVI2WBhjtqgNIbINuxGZvfztm7+vNPE6sQ -VL8i+09uoM1H6sXbe2XXORmtW0j/6MmYhSoBXNdqWTNAiyNRhwEQtowqgl5R7PBu -//QZTF1z62R9IKDMRG3f5Wn8e1Dys6tXBuG603g+Dkkc/km476mrgw== ------END RSA PRIVATE KEY-----"; - -// Generate with -// openssl ecparam -genkey -name prime256v1 | openssl pkcs8 -topk8 -nocrypt -const TEST_ECDSA_PKCS8_PEM: &str = "-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+tHxet7G+uar2Cef -iYPb7jv3uzncFtwJ7RhDOvEA0fChRANCAATCKn2AEqa9785k+TmwkeCvLub8XGrF -ezE6bA/blaPVE3nu4SUVYKULRJQxNjeOSra8TQrlIS8e5ItbMn8Tv9KV ------END PRIVATE KEY-----"; - -#[derive(Default)] -pub struct StaticKeystore { - rsa_keys: HashMap, - es256_keys: HashMap>, -} - -impl StaticKeystore { - #[must_use] - pub fn new() -> Self { - StaticKeystore::default() - } - - pub fn add_test_rsa_key(&mut self) -> anyhow::Result<()> { - let rsa = RsaPrivateKey::from_pkcs1_pem(TEST_RSA_PKCS1_PEM)?; - self.add_rsa_key(rsa)?; - Ok(()) - } - - pub fn add_test_ecdsa_key(&mut self) -> anyhow::Result<()> { - let ecdsa = SigningKey::from_pkcs8_pem(TEST_ECDSA_PKCS8_PEM)?; - self.add_ecdsa_key(ecdsa)?; - Ok(()) - } - - pub fn add_rsa_key(&mut self, key: rsa::RsaPrivateKey) -> anyhow::Result<()> { - let pubkey: &RsaPublicKey = &key; - let der = pubkey.to_pkcs1_der()?; - let digest = { - let mut digest = Sha256::new(); - digest.update(&der); - digest.finalize() - }; - // Truncate the digest to the 120 first bits - let digest = &digest[0..15]; - let digest = Base64UrlUnpadded::encode_string(digest); - let kid = format!("rsa-{}", digest); - self.rsa_keys.insert(kid, key); - Ok(()) - } - - pub fn add_ecdsa_key(&mut self, key: SigningKey) -> anyhow::Result<()> { - let pubkey: PublicKey = key.verifying_key().into(); - let der = EncodePublicKey::to_public_key_der(&pubkey)?; - let digest = { - let mut digest = Sha256::new(); - digest.update(&der); - digest.finalize() - }; - // Truncate the digest to the 120 first bits - let digest = &digest[0..15]; - let digest = Base64UrlUnpadded::encode_string(digest); - let kid = format!("ec-{}", digest); - self.es256_keys.insert(kid, key); - Ok(()) - } - - #[must_use] - pub fn to_public_jwks(&self) -> PublicJsonWebKeySet { - let rsa = self.rsa_keys.iter().map(|(kid, key)| { - let pubkey = RsaPublicKey::from(key); - JsonWebKey::new(pubkey.into()) - .with_kid(kid) - .with_use(JsonWebKeyUse::Sig) - }); - - let es256 = self.es256_keys.iter().map(|(kid, key)| { - let pubkey = ecdsa::VerifyingKey::from(key); - JsonWebKey::new(pubkey.into()) - .with_kid(kid) - .with_use(JsonWebKeyUse::Sig) - .with_alg(JsonWebSignatureAlg::Es256) - }); - - let keys = rsa.chain(es256).collect(); - PublicJsonWebKeySet::new(keys) - } - - fn verify_sync( - &self, - header: &JsonWebSignatureHeader, - payload: &[u8], - signature: &[u8], - ) -> anyhow::Result<()> { - let kid = header - .kid() - .ok_or_else(|| anyhow::anyhow!("missing kid claim in JWT header"))?; - - // TODO: do the verification in a blocking task - match header.alg() { - JsonWebSignatureAlg::Rs256 => { - let key = self - .rsa_keys - .get(kid) - .ok_or_else(|| anyhow::anyhow!("could not find RSA key in key store"))?; - - let pubkey = rsa::RsaPublicKey::from(key); - - let digest = { - let mut digest = Sha256::new(); - digest.update(&payload); - digest.finalize() - }; - - pubkey.verify( - rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_256)), - &digest, - signature, - )?; - } - - JsonWebSignatureAlg::Rs384 => { - let key = self - .rsa_keys - .get(kid) - .ok_or_else(|| anyhow::anyhow!("could not find RSA key in key store"))?; - - let pubkey = rsa::RsaPublicKey::from(key); - - let digest = { - let mut digest = Sha384::new(); - digest.update(&payload); - digest.finalize() - }; - - pubkey.verify( - rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_384)), - &digest, - signature, - )?; - } - - JsonWebSignatureAlg::Rs512 => { - let key = self - .rsa_keys - .get(kid) - .ok_or_else(|| anyhow::anyhow!("could not find RSA key in key store"))?; - - let pubkey = rsa::RsaPublicKey::from(key); - - let digest = { - let mut digest = Sha512::new(); - digest.update(&payload); - digest.finalize() - }; - - pubkey.verify( - rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_512)), - &digest, - signature, - )?; - } - - JsonWebSignatureAlg::Es256 => { - let key = self - .es256_keys - .get(kid) - .ok_or_else(|| anyhow::anyhow!("could not find ECDSA key in key store"))?; - - let pubkey = VerifyingKey::from(key); - let signature = ecdsa::Signature::from_bytes(signature)?; - - pubkey.verify(payload, &signature)?; - } - _ => bail!("unsupported algorithm"), - } - - Ok(()) - } -} - -#[async_trait] -impl SigningKeystore for StaticKeystore { - fn supported_algorithms(&self) -> HashSet { - let has_rsa = !self.rsa_keys.is_empty(); - let has_es256 = !self.es256_keys.is_empty(); - - let capacity = (if has_rsa { 3 } else { 0 }) + (if has_es256 { 1 } else { 0 }); - let mut algorithms = HashSet::with_capacity(capacity); - - if has_rsa { - algorithms.insert(JsonWebSignatureAlg::Rs256); - algorithms.insert(JsonWebSignatureAlg::Rs384); - algorithms.insert(JsonWebSignatureAlg::Rs512); - } - - if has_es256 { - algorithms.insert(JsonWebSignatureAlg::Es256); - } - - algorithms - } - - async fn prepare_header( - &self, - alg: JsonWebSignatureAlg, - ) -> anyhow::Result { - let header = JsonWebSignatureHeader::new(alg); - - let kid = match alg { - JsonWebSignatureAlg::Rs256 - | JsonWebSignatureAlg::Rs384 - | JsonWebSignatureAlg::Rs512 => self - .rsa_keys - .keys() - .next() - .ok_or_else(|| anyhow::anyhow!("no RSA keys in keystore"))?, - JsonWebSignatureAlg::Es256 => self - .es256_keys - .keys() - .next() - .ok_or_else(|| anyhow::anyhow!("no ECDSA keys in keystore"))?, - _ => bail!("unsupported algorithm"), - }; - - Ok(header.with_kid(kid)) - } - - async fn sign(&self, header: &JsonWebSignatureHeader, msg: &[u8]) -> anyhow::Result> { - let kid = header - .kid() - .ok_or_else(|| anyhow::anyhow!("missing kid from the JWT header"))?; - - // TODO: do the signing in a blocking task - let signature = match header.alg() { - JsonWebSignatureAlg::Rs256 => { - let key = self - .rsa_keys - .get(kid) - .ok_or_else(|| anyhow::anyhow!("RSA key not found in key store"))?; - - let digest = { - let mut digest = Sha256::new(); - digest.update(&msg); - digest.finalize() - }; - - key.sign( - rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_256)), - &digest, - )? - } - - JsonWebSignatureAlg::Rs384 => { - let key = self - .rsa_keys - .get(kid) - .ok_or_else(|| anyhow::anyhow!("RSA key not found in key store"))?; - - let digest = { - let mut digest = Sha384::new(); - digest.update(&msg); - digest.finalize() - }; - - key.sign( - rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_384)), - &digest, - )? - } - - JsonWebSignatureAlg::Rs512 => { - let key = self - .rsa_keys - .get(kid) - .ok_or_else(|| anyhow::anyhow!("RSA key not found in key store"))?; - - let digest = { - let mut digest = Sha512::new(); - digest.update(&msg); - digest.finalize() - }; - - key.sign( - rsa::PaddingScheme::new_pkcs1v15_sign(Some(rsa::Hash::SHA2_512)), - &digest, - )? - } - - JsonWebSignatureAlg::Es256 => { - let key = self - .es256_keys - .get(kid) - .ok_or_else(|| anyhow::anyhow!("ECDSA key not found in key store"))?; - - let signature = key.try_sign(msg)?; - let signature: &[u8] = signature.as_ref(); - signature.to_vec() - } - - _ => bail!("Unsupported algorithm"), - }; - - Ok(signature) - } -} - -impl VerifyingKeystore for StaticKeystore { - type Error = anyhow::Error; - type Future = Ready>; - - fn verify( - &self, - header: &JsonWebSignatureHeader, - msg: &[u8], - signature: &[u8], - ) -> Self::Future { - std::future::ready(self.verify_sync(header, msg, signature)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_static_store() { - let message = "this is the message to sign".as_bytes(); - let store = { - let mut s = StaticKeystore::new(); - s.add_test_rsa_key().unwrap(); - s.add_test_ecdsa_key().unwrap(); - s - }; - - for alg in [ - JsonWebSignatureAlg::Rs256, - JsonWebSignatureAlg::Rs384, - JsonWebSignatureAlg::Rs512, - JsonWebSignatureAlg::Es256, - ] { - let header = store.prepare_header(alg).await.unwrap(); - assert_eq!(header.alg(), alg); - let signature = store.sign(&header, message).await.unwrap(); - store.verify(&header, message, &signature).await.unwrap(); - } - } -} diff --git a/crates/jose/src/keystore/traits.rs b/crates/jose/src/keystore/traits.rs deleted file mode 100644 index 34f1bb4c..00000000 --- a/crates/jose/src/keystore/traits.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2022 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::{collections::HashSet, future::Future}; - -use async_trait::async_trait; -use mas_iana::jose::JsonWebSignatureAlg; - -use crate::JsonWebSignatureHeader; - -#[async_trait] -pub trait SigningKeystore { - fn supported_algorithms(&self) -> HashSet; - - async fn prepare_header( - &self, - alg: JsonWebSignatureAlg, - ) -> anyhow::Result; - - async fn sign(&self, header: &JsonWebSignatureHeader, msg: &[u8]) -> anyhow::Result>; -} - -pub trait VerifyingKeystore { - type Error; - type Future: Future>; - - fn verify(&self, header: &JsonWebSignatureHeader, msg: &[u8], signature: &[u8]) - -> Self::Future; -} diff --git a/crates/jose/src/lib.rs b/crates/jose/src/lib.rs index daa45aec..308ad46f 100644 --- a/crates/jose/src/lib.rs +++ b/crates/jose/src/lib.rs @@ -19,14 +19,8 @@ pub mod claims; pub mod constraints; -pub(crate) mod jwa; +pub mod jwa; pub mod jwk; -pub(crate) mod jwt; -mod keystore; +pub mod jwt; pub mod signer; pub mod verifier; - -pub use self::{ - jwt::{DecodedJsonWebToken, JsonWebSignatureHeader, JsonWebTokenParts, Jwt, JwtSignatureError}, - keystore::{SigningKeystore, StaticKeystore, VerifyingKeystore}, -}; diff --git a/crates/jose/src/signer.rs b/crates/jose/src/signer.rs index 080f0d7e..46067727 100644 --- a/crates/jose/src/signer.rs +++ b/crates/jose/src/signer.rs @@ -36,6 +36,78 @@ pub enum Signer { Es256K { key: jwa::Es256KSigningKey }, } +impl From for Signer { + fn from(key: jwa::Hs256Key) -> Self { + Self::Hs256 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Hs384Key) -> Self { + Self::Hs384 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Hs512Key) -> Self { + Self::Hs512 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Rs256SigningKey) -> Self { + Self::Rs256 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Rs384SigningKey) -> Self { + Self::Rs384 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Rs512SigningKey) -> Self { + Self::Rs512 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Ps256SigningKey) -> Self { + Self::Ps256 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Ps384SigningKey) -> Self { + Self::Ps384 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Ps512SigningKey) -> Self { + Self::Ps512 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Es256SigningKey) -> Self { + Self::Es256 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Es384SigningKey) -> Self { + Self::Es384 { key } + } +} + +impl From for Signer { + fn from(key: jwa::Es256KSigningKey) -> Self { + Self::Es256K { key } + } +} + #[derive(Debug, Error)] pub enum SignerFromJwkError { #[error("invalid RSA key")] diff --git a/crates/jose/src/verifier.rs b/crates/jose/src/verifier.rs index 166e7cbd..27e7e7b2 100644 --- a/crates/jose/src/verifier.rs +++ b/crates/jose/src/verifier.rs @@ -36,6 +36,78 @@ pub enum Verifier { Es256K { key: jwa::Es256KVerifyingKey }, } +impl From for Verifier { + fn from(key: jwa::Hs256Key) -> Self { + Self::Hs256 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Hs384Key) -> Self { + Self::Hs384 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Hs512Key) -> Self { + Self::Hs512 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Rs256VerifyingKey) -> Self { + Self::Rs256 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Rs384VerifyingKey) -> Self { + Self::Rs384 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Rs512VerifyingKey) -> Self { + Self::Rs512 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Ps256VerifyingKey) -> Self { + Self::Ps256 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Ps384VerifyingKey) -> Self { + Self::Ps384 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Ps512VerifyingKey) -> Self { + Self::Ps512 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Es256VerifyingKey) -> Self { + Self::Es256 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Es384VerifyingKey) -> Self { + Self::Es384 { key } + } +} + +impl From for Verifier { + fn from(key: jwa::Es256KVerifyingKey) -> Self { + Self::Es256K { key } + } +} + #[derive(Debug, Error)] pub enum VerifierFromJwkError { #[error("invalid RSA key")] diff --git a/crates/jose/tests/jws.rs b/crates/jose/tests/jws.rs index bdd8fd8d..535099a4 100644 --- a/crates/jose/tests/jws.rs +++ b/crates/jose/tests/jws.rs @@ -62,7 +62,7 @@ macro_rules! asymetric_jwt_test { use std::ops::Deref; use mas_iana::jose::JsonWebSignatureAlg; - use mas_jose::{constraints::ConstraintSet, Jwt}; + use mas_jose::{constraints::ConstraintSet, jwt::Jwt}; use super::*; @@ -95,7 +95,7 @@ macro_rules! asymetric_jwt_test { conditional! { $supported => use mas_iana::jose::JsonWebKeyUse; - use mas_jose::{constraints::Constraint, JsonWebSignatureHeader}; + use mas_jose::{constraints::Constraint, jwt::JsonWebSignatureHeader}; #[test] fn verify_jwt() { @@ -156,7 +156,7 @@ macro_rules! symetric_jwt_test { ($test_name:ident, $alg:ident, $jwt:ident) => { mod $test_name { use mas_iana::jose::JsonWebSignatureAlg; - use mas_jose::{JsonWebSignatureHeader, Jwt}; + use mas_jose::jwt::{JsonWebSignatureHeader, Jwt}; use super::*; diff --git a/crates/keystore/Cargo.toml b/crates/keystore/Cargo.toml new file mode 100644 index 00000000..9aadc633 --- /dev/null +++ b/crates/keystore/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "mas-keystore" +version = "0.1.0" +authors = ["Quentin Gliech "] +edition = "2021" +license = "Apache-2.0" + +[dependencies] +anyhow = "1.0.62" +der = { version = "0.6.0", features = ["std"] } +elliptic-curve = { version = "0.12.3", features = ["std", "pem", "sec1"] } +k256 = { version = "0.11.1", features = ["std"] } +p256 = { version = "0.11.1", features = ["std"] } +p384 = { version = "0.11.1", features = ["std"] } +pem-rfc7468 = { version = "0.6.0", features = ["std"] } +pkcs1 = { version = "0.4.0", features = ["std"] } +spki = { version = "0.6.0", features = ["std"] } +ecdsa = { version = "0.14.4", features = ["std"] } +pkcs8 = { version = "0.9.0", features = ["std", "pkcs5", "encryption"] } +const-oid = { version = "0.9.0", features = ["std"] } +rsa = { git = "https://github.com/RustCrypto/RSA.git", features = ["std", "pem"] } +sec1 = { version = "0.3.0", features = ["std"] } +thiserror = "1.0.32" +rand_core = "0.6.3" + +mas-iana = { path = "../iana" } +mas-jose = { path = "../jose" } diff --git a/crates/keystore/src/lib.rs b/crates/keystore/src/lib.rs new file mode 100644 index 00000000..7da9b474 --- /dev/null +++ b/crates/keystore/src/lib.rs @@ -0,0 +1,652 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A crate to store keys which can then be used to sign and verify JWTs. + +#![forbid(unsafe_code)] +#![deny( + clippy::all, + clippy::str_to_string, + rustdoc::broken_intra_doc_links, + rustdoc::all +)] +#![warn(clippy::pedantic)] + +use std::sync::Arc; + +use der::{zeroize::Zeroizing, Decode}; +use mas_iana::jose::{JsonWebKeyType, JsonWebSignatureAlg}; +pub use mas_jose::jwk::{JsonWebKey, JsonWebKeySet}; +use mas_jose::{ + constraints::{Constraint, ConstraintSet}, + jwk::{JsonWebKeyPublicParameters, ParametersInfo, PublicJsonWebKeySet}, + signer::Signer, + verifier::Verifier, +}; +use pem_rfc7468::PemLabel; +use pkcs1::EncodeRsaPrivateKey; +use pkcs8::{AssociatedOid, PrivateKeyInfo}; +use rand_core::{CryptoRng, RngCore}; +use rsa::BigUint; +use sec1::EncodeEcPrivateKey; +use thiserror::Error; + +/// Error type used when a key could not be loaded +#[derive(Debug, Error)] +pub enum LoadError { + #[error("Failed to read PEM document")] + Pem { + #[from] + inner: pem_rfc7468::Error, + }, + + #[error("Invalid RSA private key")] + Rsa { + #[from] + inner: rsa::errors::Error, + }, + + #[error("Failed to decode PKCS1-encoded RSA key")] + Pkcs1 { + #[from] + inner: pkcs1::Error, + }, + + #[error("Failed to decode PKCS8-encoded key")] + Pkcs8 { + #[from] + inner: pkcs8::Error, + }, + + #[error(transparent)] + Der { + #[from] + inner: der::Error, + }, + + #[error(transparent)] + Spki { + #[from] + inner: spki::Error, + }, + + #[error("Unknown Elliptic Curve OID {oid}")] + UnknownEllipticCurveOid { oid: const_oid::ObjectIdentifier }, + + #[error("Unknown algorithm OID {oid}")] + UnknownAlgorithmOid { oid: const_oid::ObjectIdentifier }, + + #[error("Unsupported PEM label {label:?}")] + UnsupportedPemLabel { label: String }, + + #[error("Missing parameters in SEC1 key")] + MissingSec1Parameters, + + #[error("Missing curve name in SEC1 parameters")] + MissingSec1CurveName, + + #[error("Key is encrypted and no password was provided")] + Encrypted, + + #[error("Key is not encrypted but a password was provided")] + Unencrypted, + + #[error("Unsupported format")] + UnsupportedFormat, + + #[error("Could not decode encrypted payload")] + InEncrypted { + #[source] + inner: Box, + }, +} + +/// A single private key +#[non_exhaustive] +pub enum PrivateKey { + Rsa(Box), + EcP256(Box>), + EcP384(Box>), + EcK256(Box>), +} + +/// Error returned when the key can't be used for the requested algorithm +#[derive(Debug, Error)] +#[error("Wrong algorithm for key")] +pub struct WrongAlgorithmError; + +impl PrivateKey { + fn from_pkcs1_private_key(pkcs1_key: &pkcs1::RsaPrivateKey) -> Result { + // Taken from `TryFrom> for RsaPrivateKey` + + // Multi-prime RSA keys not currently supported + if pkcs1_key.version() != pkcs1::Version::TwoPrime { + return Err(pkcs1::Error::Version.into()); + } + + let n = BigUint::from_bytes_be(pkcs1_key.modulus.as_bytes()); + let e = BigUint::from_bytes_be(pkcs1_key.public_exponent.as_bytes()); + let d = BigUint::from_bytes_be(pkcs1_key.private_exponent.as_bytes()); + let first_prime = BigUint::from_bytes_be(pkcs1_key.prime1.as_bytes()); + let second_prime = BigUint::from_bytes_be(pkcs1_key.prime2.as_bytes()); + let primes = vec![first_prime, second_prime]; + let key = rsa::RsaPrivateKey::from_components(n, e, d, primes)?; + Ok(Self::Rsa(Box::new(key))) + } + + fn from_private_key_info(info: PrivateKeyInfo) -> Result { + match info.algorithm.oid { + pkcs1::ALGORITHM_OID => Ok(Self::Rsa(Box::new(info.try_into()?))), + elliptic_curve::ALGORITHM_OID => match info.algorithm.parameters_oid()? { + p256::NistP256::OID => Ok(Self::EcP256(Box::new(info.try_into()?))), + p384::NistP384::OID => Ok(Self::EcP384(Box::new(info.try_into()?))), + k256::Secp256k1::OID => Ok(Self::EcK256(Box::new(info.try_into()?))), + oid => Err(LoadError::UnknownEllipticCurveOid { oid }), + }, + oid => Err(LoadError::UnknownAlgorithmOid { oid }), + } + } + + fn from_ec_private_key(key: sec1::EcPrivateKey) -> Result { + let curve = key + .parameters + .ok_or(LoadError::MissingSec1Parameters)? + .named_curve() + .ok_or(LoadError::MissingSec1CurveName)?; + + match curve { + p256::NistP256::OID => Ok(Self::EcP256(Box::new(key.try_into()?))), + p384::NistP384::OID => Ok(Self::EcP384(Box::new(key.try_into()?))), + k256::Secp256k1::OID => Ok(Self::EcK256(Box::new(key.try_into()?))), + oid => Err(LoadError::UnknownEllipticCurveOid { oid }), + } + } + + /// Serialize the key as a DER document + /// + /// It will use the most common format depending on the key type: PKCS1 for + /// RSA keys and SEC1 for elliptic curve keys + /// + /// # Errors + /// + /// Returns an error if the encoding failed + pub fn to_der(&self) -> Result>, anyhow::Error> { + let der = match self { + PrivateKey::Rsa(key) => key.to_pkcs1_der()?.to_bytes(), + PrivateKey::EcP256(key) => key.to_sec1_der()?, + PrivateKey::EcP384(key) => key.to_sec1_der()?, + PrivateKey::EcK256(key) => key.to_sec1_der()?, + }; + + Ok(der) + } + + /// Serialize the key as a PEM document + /// + /// It will use the most common format depending on the key type: PKCS1 for + /// RSA keys and SEC1 for elliptic curve keys + /// + /// # Errors + /// + /// Returns an error if the encoding failed + pub fn to_pem( + &self, + line_ending: pem_rfc7468::LineEnding, + ) -> Result, anyhow::Error> { + let pem = match self { + PrivateKey::Rsa(key) => key.to_pkcs1_pem(line_ending)?, + PrivateKey::EcP256(key) => key.to_sec1_pem(line_ending)?, + PrivateKey::EcP384(key) => key.to_sec1_pem(line_ending)?, + PrivateKey::EcK256(key) => key.to_sec1_pem(line_ending)?, + }; + + Ok(pem) + } + + /// Load an unencrypted PEM or DER encoded key + /// + /// # Errors + /// + /// Returns the same kind of errors as [`Self::load_pem`] and + /// [`Self::load_der`]. + pub fn load(bytes: &[u8]) -> Result { + if let Ok(pem) = std::str::from_utf8(bytes) { + match Self::load_pem(pem) { + Ok(s) => return Ok(s), + // If there was an error loading the document as PEM, ignore it and continue by + // trying to load it as DER + Err(LoadError::Pem { .. }) => {} + Err(e) => return Err(e), + } + } + + Self::load_der(bytes) + } + + /// Load an encrypted PEM or DER encoded key, and decrypt it with the given + /// password + /// + /// # Errors + /// + /// Returns the same kind of errors as [`Self::load_encrypted_pem`] and + /// [`Self::load_encrypted_der`]. + pub fn load_encrypted(bytes: &[u8], password: impl AsRef<[u8]>) -> Result { + if let Ok(pem) = std::str::from_utf8(bytes) { + match Self::load_encrypted_pem(pem, password.as_ref()) { + Ok(s) => return Ok(s), + // If there was an error loading the document as PEM, ignore it and continue by + // trying to load it as DER + Err(LoadError::Pem { .. }) => {} + Err(e) => return Err(e), + } + } + + Self::load_encrypted_der(bytes, password) + } + + /// Load an encrypted key from DER-encoded bytes, and decrypt it with the + /// given password + /// + /// # Errors + /// + /// Returns an error if: + /// - the key is in an non-encrypted format + /// - the key could not be decrypted + /// - the PKCS8 key could not be loaded + pub fn load_encrypted_der(der: &[u8], password: impl AsRef<[u8]>) -> Result { + if let Ok(info) = pkcs8::EncryptedPrivateKeyInfo::from_der(der) { + let decrypted = info.decrypt(password)?; + return Self::load_der(decrypted.as_bytes()).map_err(|inner| LoadError::InEncrypted { + inner: Box::new(inner), + }); + } + + if pkcs8::PrivateKeyInfo::from_der(der).is_ok() + || sec1::EcPrivateKey::from_der(der).is_ok() + || pkcs1::RsaPrivateKey::from_der(der).is_ok() + { + return Err(LoadError::Encrypted); + } + + Err(LoadError::UnsupportedFormat) + } + + /// Load an unencrypted key from DER-encoded bytes + /// + /// It tries to decode the bytes from the various known DER formats (PKCS8, + /// SEC1 and PKCS1, in that order), and return the first one that works. + /// + /// # Errors + /// + /// Returns an error if: + /// - the PKCS8 key is encrypted + /// - none of the formats could be decoded + /// - the PKCS8/SEC1/PKCS1 key could not be loaded + pub fn load_der(der: &[u8]) -> Result { + // Let's try evey known DER format one after the other + if pkcs8::EncryptedPrivateKeyInfo::from_der(der).is_ok() { + return Err(LoadError::Encrypted); + } + + if let Ok(info) = pkcs8::PrivateKeyInfo::from_der(der) { + return Self::from_private_key_info(info); + } + + if let Ok(info) = sec1::EcPrivateKey::from_der(der) { + return Self::from_ec_private_key(info); + } + + if let Ok(pkcs1_key) = pkcs1::RsaPrivateKey::from_der(der) { + return Self::from_pkcs1_private_key(&pkcs1_key); + } + + Err(LoadError::UnsupportedFormat) + } + + /// Load an encrypted key from a PEM-encode string, and decrypt it with the + /// given password + /// + /// # Errors + /// + /// Returns an error if: + /// - the file is not a signel PEM document + /// - the PEM label is not a supported format + /// - the underlying key is not encrypted (use [`Self::load`] instead) + /// - the decryption failed + /// - the pkcs8 key could not be loaded + pub fn load_encrypted_pem(pem: &str, password: impl AsRef<[u8]>) -> Result { + let (label, doc) = pem_rfc7468::decode_vec(pem.as_bytes())?; + + match label { + pkcs8::EncryptedPrivateKeyInfo::PEM_LABEL => { + let info = pkcs8::EncryptedPrivateKeyInfo::from_der(&doc)?; + let decrypted = info.decrypt(password)?; + return Self::load_der(decrypted.as_bytes()).map_err(|inner| { + LoadError::InEncrypted { + inner: Box::new(inner), + } + }); + } + + pkcs1::RsaPrivateKey::PEM_LABEL + | pkcs8::PrivateKeyInfo::PEM_LABEL + | sec1::EcPrivateKey::PEM_LABEL => Err(LoadError::Unencrypted), + + label => Err(LoadError::UnsupportedPemLabel { + label: label.to_owned(), + }), + } + } + + /// Load an unencrypted key from a PEM-encode string + /// + /// # Errors + /// + /// Returns an error if: + /// - the file is not a signel PEM document + /// - the PEM label is not a supported format + /// - the underlying key is encrypted (use [`Self::load_encrypted`] + /// instead) + /// - the PKCS8/PKCS1/SEC1 key could not be loaded + pub fn load_pem(pem: &str) -> Result { + let (label, doc) = pem_rfc7468::decode_vec(pem.as_bytes())?; + + match label { + pkcs1::RsaPrivateKey::PEM_LABEL => { + let pkcs1_key = pkcs1::RsaPrivateKey::from_der(&doc)?; + Self::from_pkcs1_private_key(&pkcs1_key) + } + + pkcs8::PrivateKeyInfo::PEM_LABEL => { + let info = pkcs8::PrivateKeyInfo::from_der(&doc)?; + Self::from_private_key_info(info) + } + + sec1::EcPrivateKey::PEM_LABEL => { + let key = sec1::EcPrivateKey::from_der(&doc)?; + Self::from_ec_private_key(key) + } + + pkcs8::EncryptedPrivateKeyInfo::PEM_LABEL => Err(LoadError::Encrypted), + + label => Err(LoadError::UnsupportedPemLabel { + label: label.to_owned(), + }), + } + } + + /// Get a [`Verifier`] out of this key, for the specified + /// [`JsonWebSignatureAlg`] + /// + /// # Errors + /// + /// Returns an error if the key is not suited for the selected algorithm + pub fn verifier_for_alg( + &self, + alg: JsonWebSignatureAlg, + ) -> Result { + let signer = match (self, alg) { + (Self::Rsa(key), JsonWebSignatureAlg::Rs256) => { + mas_jose::jwa::Rs256VerifyingKey::from(key.to_public_key()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Rs384) => { + mas_jose::jwa::Rs384VerifyingKey::from(key.to_public_key()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Rs512) => { + mas_jose::jwa::Rs512VerifyingKey::from(key.to_public_key()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Ps256) => { + mas_jose::jwa::Ps256VerifyingKey::from(key.to_public_key()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Ps384) => { + mas_jose::jwa::Ps384VerifyingKey::from(key.to_public_key()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Ps512) => { + mas_jose::jwa::Ps512VerifyingKey::from(key.to_public_key()).into() + } + + (Self::EcP256(key), JsonWebSignatureAlg::Es256) => { + mas_jose::jwa::Es256VerifyingKey::from(key.public_key()).into() + } + + (Self::EcP384(key), JsonWebSignatureAlg::Es384) => { + mas_jose::jwa::Es384VerifyingKey::from(key.public_key()).into() + } + + (Self::EcK256(key), JsonWebSignatureAlg::Es256K) => { + mas_jose::jwa::Es256KVerifyingKey::from(key.public_key()).into() + } + + _ => return Err(WrongAlgorithmError), + }; + + Ok(signer) + } + + /// Get a [`Signer`] out of this key, for the specified + /// [`JsonWebSignatureAlg`] + /// + /// # Errors + /// + /// Returns an error if the key is not suited for the selected algorithm + pub fn signer_for_alg(&self, alg: JsonWebSignatureAlg) -> Result { + let signer = match (self, alg) { + (Self::Rsa(key), JsonWebSignatureAlg::Rs256) => { + mas_jose::jwa::Rs256SigningKey::from(*key.clone()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Rs384) => { + mas_jose::jwa::Rs384SigningKey::from(*key.clone()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Rs512) => { + mas_jose::jwa::Rs512SigningKey::from(*key.clone()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Ps256) => { + mas_jose::jwa::Ps256SigningKey::from(*key.clone()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Ps384) => { + mas_jose::jwa::Ps384SigningKey::from(*key.clone()).into() + } + + (Self::Rsa(key), JsonWebSignatureAlg::Ps512) => { + mas_jose::jwa::Ps512SigningKey::from(*key.clone()).into() + } + + (Self::EcP256(key), JsonWebSignatureAlg::Es256) => { + mas_jose::jwa::Es256SigningKey::from(*key.clone()).into() + } + + (Self::EcP384(key), JsonWebSignatureAlg::Es384) => { + mas_jose::jwa::Es384SigningKey::from(*key.clone()).into() + } + + (Self::EcK256(key), JsonWebSignatureAlg::Es256K) => { + mas_jose::jwa::Es256KSigningKey::from(*key.clone()).into() + } + + _ => return Err(WrongAlgorithmError), + }; + + Ok(signer) + } + + /// Generate a RSA key with 2048 bit size + /// + /// # Errors + /// + /// Returns any error from the underlying key generator + pub fn generate_rsa(mut rng: R) -> Result { + let key = rsa::RsaPrivateKey::new(&mut rng, 2048)?; + Ok(Self::Rsa(Box::new(key))) + } + + /// Generate an Elliptic Curve key for the P-256 curve + pub fn generate_ec_p256(rng: R) -> Self { + let key = elliptic_curve::SecretKey::random(rng); + Self::EcP256(Box::new(key)) + } + + /// Generate an Elliptic Curve key for the P-384 curve + pub fn generate_ec_p384(rng: R) -> Self { + let key = elliptic_curve::SecretKey::random(rng); + Self::EcP384(Box::new(key)) + } + + /// Generate an Elliptic Curve key for the secp256k1 curve + pub fn generate_ec_k256(rng: R) -> Self { + let key = elliptic_curve::SecretKey::random(rng); + Self::EcK256(Box::new(key)) + } +} + +impl From<&PrivateKey> for JsonWebKeyPublicParameters { + fn from(val: &PrivateKey) -> Self { + match val { + PrivateKey::Rsa(key) => key.to_public_key().into(), + PrivateKey::EcP256(key) => { + let key: ecdsa::VerifyingKey<_> = key.public_key().into(); + key.into() + } + PrivateKey::EcP384(key) => { + let key: ecdsa::VerifyingKey<_> = key.public_key().into(); + key.into() + } + PrivateKey::EcK256(key) => { + let key: ecdsa::VerifyingKey<_> = key.public_key().into(); + key.into() + } + } + } +} + +impl ParametersInfo for PrivateKey { + fn kty(&self) -> JsonWebKeyType { + match self { + PrivateKey::Rsa(_) => JsonWebKeyType::Rsa, + PrivateKey::EcP256(_) | PrivateKey::EcP384(_) | PrivateKey::EcK256(_) => { + JsonWebKeyType::Ec + } + } + } + + fn possible_algs(&self) -> &'static [JsonWebSignatureAlg] { + match self { + PrivateKey::Rsa(_) => &[ + JsonWebSignatureAlg::Rs256, + JsonWebSignatureAlg::Rs384, + JsonWebSignatureAlg::Rs512, + JsonWebSignatureAlg::Ps256, + JsonWebSignatureAlg::Ps384, + JsonWebSignatureAlg::Ps512, + ], + PrivateKey::EcP256(_) => &[JsonWebSignatureAlg::Es256], + PrivateKey::EcP384(_) => &[JsonWebSignatureAlg::Es384], + PrivateKey::EcK256(_) => &[JsonWebSignatureAlg::Es256K], + } + } +} + +/// A structure to store a list of [`PrivateKey`]. The keys are held in an +/// [`Arc`] to ensure they are only loaded once in memory and allow cheap +/// cloning +#[derive(Clone, Default)] +pub struct Keystore { + keys: Arc>, +} + +impl Keystore { + /// Create a keystore out of a JSON Web Key Set + /// + /// ```rust + /// use mas_keystore::{Keystore, PrivateKey, JsonWebKey, JsonWebKeySet}; + /// let rsa = PrivateKey::load_pem(include_str!("../tests/keys/rsa.pkcs1.pem")).unwrap(); + /// let rsa = JsonWebKey::new(rsa); + /// + /// let ec_p256 = PrivateKey::load_pem(include_str!("../tests/keys/ec-p256.sec1.pem")).unwrap(); + /// let ec_p256 = JsonWebKey::new(ec_p256); + /// + /// let ec_p384 = PrivateKey::load_pem(include_str!("../tests/keys/ec-p384.sec1.pem")).unwrap(); + /// let ec_p384 = JsonWebKey::new(ec_p384); + /// + /// let ec_k256 = PrivateKey::load_pem(include_str!("../tests/keys/ec-k256.sec1.pem")).unwrap(); + /// let ec_k256 = JsonWebKey::new(ec_k256); + /// + /// let jwks = JsonWebKeySet::new(vec![rsa, ec_p256, ec_p384, ec_k256]); + /// let keystore = Keystore::new(jwks); + /// ``` + #[must_use] + pub fn new(keys: JsonWebKeySet) -> Self { + let keys = Arc::new(keys); + Self { keys } + } + + /// Get the public JSON Web Key Set for the keys stored in this [`Keystore`] + #[must_use] + pub fn public_jwks(&self) -> PublicJsonWebKeySet { + self.keys + .iter() + .map(|key| { + key.cloned_map(|params: &PrivateKey| JsonWebKeyPublicParameters::from(params)) + }) + .collect() + } + + /// Find the best key given the constraints + #[must_use] + pub fn find_key(&self, constraints: &ConstraintSet) -> Option<&JsonWebKey> { + constraints.filter(self.keys.iter()).pop() + } + + /// Find the list of keys which match the givent constraints + #[must_use] + pub fn find_keys(&self, constraints: &ConstraintSet) -> Vec<&JsonWebKey> { + constraints.filter(self.keys.iter()) + } + + /// Find a key for the given algorithm. Returns `None` if no suitable key + /// was found. + #[must_use] + pub fn signing_key_for_algorithm( + &self, + alg: JsonWebSignatureAlg, + ) -> Option<&JsonWebKey> { + let constraints = ConstraintSet::new([ + Constraint::alg(alg), + Constraint::use_(mas_iana::jose::JsonWebKeyUse::Sig), + ]); + self.find_key(&constraints) + } + + /// Get a list of available signing algorithms for this [`Keystore`] + #[must_use] + pub fn available_signing_algorithms(&self) -> Vec { + let mut algs: Vec<_> = self + .keys + .iter() + .flat_map(|key| key.params().possible_algs()) + .copied() + .collect(); + algs.sort(); + algs.dedup(); + algs + } +} diff --git a/crates/keystore/tests/generate.sh b/crates/keystore/tests/generate.sh new file mode 100644 index 00000000..c2de69c9 --- /dev/null +++ b/crates/keystore/tests/generate.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +set -eux + +KEYS="$(dirname "$0")/keys" +mkdir -p "${KEYS}" + +export PASSWORD="hunter2" + +convert() { + FILE=$1 + NAME=$2 + openssl asn1parse -noout -in "${KEYS}/${FILE}.pem" -out "${KEYS}/${FILE}.der" + openssl pkcs8 -topk8 -nocrypt -in "${KEYS}/${FILE}.pem" -out "${KEYS}/${NAME}.pkcs8.pem" + openssl asn1parse -noout -in "${KEYS}/${NAME}.pkcs8.pem" -out "${KEYS}/${NAME}.pkcs8.der" + openssl pkcs8 -topk8 -passout env:PASSWORD -in "${KEYS}/${FILE}.pem" -out "${KEYS}/${NAME}.pkcs8.encrypted.pem" + openssl asn1parse -noout -in "${KEYS}/${NAME}.pkcs8.encrypted.pem" -out "${KEYS}/${NAME}.pkcs8.encrypted.der" +} + +openssl genrsa -out "${KEYS}/rsa.pkcs1.pem" 2048 +convert "rsa.pkcs1" "rsa" + +openssl ecparam -genkey -name prime256v1 -noout -out "${KEYS}/ec-p256.sec1.pem" +convert "ec-p256.sec1" "ec-p256" + +openssl ecparam -genkey -name secp384r1 -noout -out "${KEYS}/ec-p384.sec1.pem" +convert "ec-p384.sec1" "ec-p384" + +openssl ecparam -genkey -name secp256k1 -noout -out "${KEYS}/ec-k256.sec1.pem" +convert "ec-k256.sec1" "ec-k256" diff --git a/crates/keystore/tests/keys/ec-k256.pkcs8.der b/crates/keystore/tests/keys/ec-k256.pkcs8.der new file mode 100644 index 0000000000000000000000000000000000000000..793d3987db9c03c338e3cc1c5569d9b1f0c27b2d GIT binary patch literal 135 zcmV;20C@i}frJ7905A{+2P%e0&OHJF1_djD1ON&IZ7^#B0RaRcPcnH(b=OmAs_mZV z1@Vk8#sisK8SNv`rB|tw_1gNPp+o~h00c;<)U>9Hs3+ko>UYo1^jn{DIg8!;NXA+3 ptt(!VnQYl5v_gT=+K_QMFfOen8Si=97@(xNv_5;QZvJtBxB7L#XbdL<2$q1W-ZWC#pr2DO^#RoO&0ZU6uP literal 0 HcmV?d00001 diff --git a/crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.der b/crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.der new file mode 100644 index 0000000000000000000000000000000000000000..e0d77bb0c994c4b44b1d3bd212b11a961b328d6c GIT binary patch literal 239 zcmXqLd}9#K#;Mij(e|B}k(JlL%Rm#sZxgMo&u;7w>vsbHcOqeZ literal 0 HcmV?d00001 diff --git a/crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.pem b/crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.pem new file mode 100644 index 00000000..e8daf6db --- /dev/null +++ b/crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.pem @@ -0,0 +1,7 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhIOTdQ9pS7EgICCAAw +DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEEVvTsSwG1HEr6urEKUSC8kEgZCQ +fLQHNDHSjGin9RvcMYi5htsKZbRJK1JL19o7cf8W4AH0kKNAlDtJBrc7j/9tlCkP +b/7O7KFCNkeCrfF113mzgoRuD4xLzoe3n+ybpeBgf8WJuJowiZwhKGXGlUP/m+XX +aWiCKUaaA4huhJbQzJDBdVUnKEZZ+lysEMjYjNgplGc2uvoNSywWKHubgY9Wj0Y= +-----END ENCRYPTED PRIVATE KEY----- diff --git a/crates/keystore/tests/keys/ec-p256.pkcs8.pem b/crates/keystore/tests/keys/ec-p256.pkcs8.pem new file mode 100644 index 00000000..07a7aede --- /dev/null +++ b/crates/keystore/tests/keys/ec-p256.pkcs8.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5Ru1AmWbX0F4p3X0 +8YIWMnVm+6KJqQiIjm0Pw2BDqO6hRANCAARQQd/kCEAv7PYjKvA+xhQAvnQXNbXZ +fXfUHEiuBjpV2b70TZCr08POfUZf/BjTHG+NuluyLFle6dJWIga1muhV +-----END PRIVATE KEY----- diff --git a/crates/keystore/tests/keys/ec-p256.sec1.der b/crates/keystore/tests/keys/ec-p256.sec1.der new file mode 100644 index 0000000000000000000000000000000000000000..6a9f8b3998829d0bd77b522e755d63f3e717f90d GIT binary patch literal 121 zcmV-<0EYiCcLD(c1R&)bwE|_EUqN`Mb@cIq7BY2a`=W`d2#Aht55r(XsP3Q&1_&yK zNX|V20SBQ(13~}X zJkO{oXj<`~Yk~AtIFnW|bZ##EIUeE(h6)>d+4aRnvUC{x{&}Hf17ZLKteG9X*4YJX z=9h5QDh1~`=28V>G;e6D&Of`$?sTKn@vJ@zo&AA(!+r8^o}a)@$MizR-|gch&AR@) neI=ZVy0qto1!P(8*&H9a^g5(_+`!B=*3$H5I)Os1lKdeY=L1%H literal 0 HcmV?d00001 diff --git a/crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.der b/crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.der new file mode 100644 index 0000000000000000000000000000000000000000..1d8cab4b1368a9924eed81688c57fdcdf3d4f55f GIT binary patch literal 288 zcmXqLVw5omXXDgr^Jx3d%gD-W;ANnR;PM#Auy74nJ+epWR>XB=ks`HMs+Zz%Vu583qT!uR!i z?)J)U|HG!!o)z7*zJ9HSN3F%pqM2If7XRt%X0CEEVc+~^onoBPB=d`I6Yg7WeqXof z^@fL6z0Or{KW}NZ>p}kiY44mlc@6&N>vXX5$)>0MSQZ!l%F2D_I-TChix>9XJ=y7- h-0MEGNkG|RvDMj2!(BX*YojtC;(}GcJBZH literal 0 HcmV?d00001 diff --git a/crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.pem b/crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.pem new file mode 100644 index 00000000..524d25b2 --- /dev/null +++ b/crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.pem @@ -0,0 +1,8 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIIBHDBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIYMIe05yFZUgCAggA +MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBAlgotQyGiZyH4G0SlIKij5BIHA +0vyLKiFzcUxy5Ch1FGWx4WpZlzhBKwk4ZPxKBH18/DXVbC9yfZJR5dCTgE46fFLM +QJmOTxRbY7B3SH9UsvLQ96+83Y0et/wGLIdqW4yvf60oSH042XKZKs6j/I6LA3pE +NAez9K4hXjKSN9FGkN86s+9+ouuw4dVKznu3zzk6uuBv/5buQwkNMP1vLIgHDh1n +ZvimXlfqOkeZriyNk6OhjN3JiU1jjUeZghAjOKM6o6U5CYAi9KgCDBJWtu6M8edV +-----END ENCRYPTED PRIVATE KEY----- diff --git a/crates/keystore/tests/keys/ec-p384.pkcs8.pem b/crates/keystore/tests/keys/ec-p384.pkcs8.pem new file mode 100644 index 00000000..ed487587 --- /dev/null +++ b/crates/keystore/tests/keys/ec-p384.pkcs8.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB7jIEkTYf/O4QMPM+o +KGha8Z9rgfRVOJNWMHRuLvw5HuIJhgobe9n1xUaydBj7/nmhZANiAASsmR291tkF +a+aXcNUqBec55lIFYjRvaKvOP7vL7nSj1PGsPgud/YF7w33yb56fwE7H9ELG3+3j +JM26/rx9JZyKurTnhQVkWe/ZHB+59Dqke9zAzDXW0vRmOoFCrZL8IRw= +-----END PRIVATE KEY----- diff --git a/crates/keystore/tests/keys/ec-p384.sec1.der b/crates/keystore/tests/keys/ec-p384.sec1.der new file mode 100644 index 0000000000000000000000000000000000000000..4ab9beeb545bf6d73e5aad95fbe3eab2307ddbb4 GIT binary patch literal 167 zcmV;Y09gMpfusTf0R%96jDaLghyOc-3_Q=MC}>*opKF2iRXCGYFm!G%{5c-t35E(A zd)f8HMzVAm`~G>L2L=Tzfdl{|p=1MM00gX=9lh4s1#9M)aMdaW=Q-w51!6RBXsgaY zyUXr$qtx-NJ`0`wfqTP!@^7A>z)r{XLdM_i<0Q?x{=9u9oQk@%=Y<7iS?}2#AG!28 VqCoiA#UA2l93wrg#FtK8NMXq~p ziPDsg(x5AchTJ=k>j0E+C8;xl0bKe&P5oo=2E6XEY%bsX)!(&gTWmKYC|4V$2&ItU zYPLs2AGFJBBLEcZAU5!2z+Zl%KXByoB;NOtP0&{(fu$ZeVa}nMDkN#3|3{xE2Ojsb zPvE!S`=LV0Ict0zP}{E!Q=-Kxk>X3zw}lm0(!VNU5Smq#*ZI8)QZTO15bxTw#zC4% zts=N?w4V99wU@K?F=WRA0|5X50)hbma5-X1X+yWkN^Jwc(83+;fCSQ3o(;Dm>=^*% z)i)4zZZwVp|1vfNxXr9TA09!SZ!(KnSY^MK(;cphmHUI;W72st5-wo#$q95&22R5{ zqD=u)2?wx1V@*TI04Ar5X!2{1C43X~IAw|~l{Nv!<8`fCc|*md@YloqI&LR~wiGa~ zink1KjGj)S^}8{EgO_;SIEp`425Sc>@Q>OM6rGf%$Cuu1hsP_w*7uf)r){ch|K1%h z94Q7uCr_P1=5Q_XV@E3HyHs;!x758DYb8V-Q;^43&>}(h zRD}&}1io$ca{7*z2^l)GRl|nC0)c@5-%E0lRL$hM++&B3BMdf~Z!gw8L>sB#RVYo| zYk^MX*Jwk5@H?EzKJq5rS%zkbz{xmj37OaT|7CD(e6&OvT-CnpDn*}*9lD6h7_`9x zK+&hOK8s$tG$$5+h`r&)wv-JGfs6e}Bzqfz#kV#b=0@jIX}WNV(Kl^X^S|`v0)c@5 z)jN{eU>D5WokVT%cm587*$WTQFMTWWj}42H3W71cz~wq2$B^9WS zcVI@eXNp0>YVgMd?FInv8#aUMqo znK;@bzM3=Ybn1o-4T0)c>S`30Kj4r6HAPd1a;e+gjq?zC_Q94*Bw zi>DPh={<*ASfOs*cQ2Q@<)0XXANyW9(VuiN_)qe88EKBP%|U?BMFBhH@z+YXz=z-{ z+|>3QAZ47C;3X}`uDqDxD$9t6p-}KFo}8&Z%rJUDYz=^Jw@l1{Y( zfq+^O8O$4;vH2;Am+j!gJsIQInoLt-$;V*HRpmSp7B!My!;$W$`e*IWlSU04BXZJU=kZ!OB#8bHh<>DvF Ftj~+FNCN-> literal 0 HcmV?d00001 diff --git a/crates/keystore/tests/keys/rsa.pkcs1.pem b/crates/keystore/tests/keys/rsa.pkcs1.pem new file mode 100644 index 00000000..74ffc74c --- /dev/null +++ b/crates/keystore/tests/keys/rsa.pkcs1.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAuf28zPUp574jDRdX6uN0d7niZCIUpACFo+Po/13FuIGsrpze +yMX6CYWVPalgXW9FCrhxL+4toJRy5npjkgsLFsknL5/zXbWKFgt69cMwsWJ9Ra57 +bonSlI7SoCuHhtw7j+sAlHAlqTOCAVz6P039Y/AGvO6xbC7f+9XftWlbbDcjKFcb +pQilkN9qtkdEH7TLayMAFOsgNvBlwF9+oj9w5PIk3veRTdBXI4GlHjhhzqGZKiRp +oP9HnycHHveyT+C33vuhQso5a3wcUNuvDVOixSqR4kvSt4UVWNK/KmEQmlWU1/m9 +ClIwrs8Q79q0xkGaSa0iuG60nvm7tZez9TFkxwIDAQABAoIBAHA5YkppQ7fJSm0D +wNDCHeyABNJWng23IuwZAOXVNxB1bjSOAv8yNgS4zaw/Hx5BnW8yi1lYZb+W0x2u +i5X7g91j0nkyEi5g88kJdFAGTsM5ok0BUwkHsEBjTUPIACanjGjya48lfBP0OGWK +LJU2Acbjda1aeUPFpPDXw/w6bieEthQwroq3DHCMnk6i9bsxgIOXeN04ij9XBmsH +KPCP2hAUnZSlx5febYfHK7/W95aJp22qa//eHS8cKQZCJ0+dQuZwLhlGosTFqLUm +qhPlt/b1EvPPY0cq5rtUc2W31L0YayVEHVOQx1fQIkH2VIUNbAS+bfVy+o6WCRk6 +s1XDhsECgYEA30tykVTN5LncY4eQIww2mW8v1j1EG6ngVShN3GuBTuXXaEOB8Duc +yT7yJt1ZhmaJwMk4agmZ1/f/ZXBtfLREGVzVvuwqRZ+LHbqIyhi0wQJA0aezPote +uTQnFn+IveHGtpQNDYGL/UgkexuCxbc2HOZG51JpunCK0TdtVfO/9OUCgYEA1TuS +2WAXzNudRG3xd/4OgtkLD9AvfSvyjw2LkwqCMb3A5UEqw7vubk/xgnRvqrAgJRWo +jndgRrRnikHCavDHBO0GAO/kzrFRfw+e+r4jcLl0Yadke8ndCc7VTnx4wQCrMi5H +7HEeRwaZONoj5PAPyA5X+N/gT0NNDA7KoQT45DsCgYBt+QWa6A5jaNpPNpPZfwlg +9e60cAYcLcUri6cVOOk9h1tYoW7cdy+XueWfGIMf+1460Z90MfhP8ncZaY6yzUGA +0EUBO+Tx10q3wIfgKNzU9hwgZZyU4CUtx668mOEqy4iHoVDwZu4gNyiobPsyDzKa +dxtSkDc8OHNV6RtzKpJOtQKBgFoRGcwbnLH5KYqX7eDDPRnj15pMU2LJx2DJVeU8 +ERY1kl7Dke6vWNzbg6WYzPoJ/unrJhFXNyFmXj213QsSvN3FyD1pFvp/R28mB/7d +hVa93vzImdb3wxe7d7n5NYBAag9+IP8sIJ/bl6i9619uTxwvgtUqqzKPuOGY9dnh +oce1AoGBAKZyZc/NVgqV2KgAnnYlcwNn7sRSkM8dcq0/gBMNuSZkfZSuEd4wwUzR +iFlYp23O2nHWggTkzimuBPtD7Kq4jBey3ZkyGye+sAdmnKkOjNILNbpIZlT6gK3z +fBaFmJGRJinKA+BJeH79WFpYN6SBZ/c3s5BusAbEU7kE5eInyazP +-----END RSA PRIVATE KEY----- diff --git a/crates/keystore/tests/keys/rsa.pkcs8.der b/crates/keystore/tests/keys/rsa.pkcs8.der new file mode 100644 index 0000000000000000000000000000000000000000..7863b1094f6f91de09a6ba9dd9279355e429ff43 GIT binary patch literal 1217 zcmV;y1U~yPf&{$+0RS)!1_>&LNQUrr!ay9qXGc{0)hbn0J;6V%=Ibf zz9S76SL)+*ce&zZA{3+mg`?x>|6Rqnfvm2a-pIxJ35AtCsbF1iMGClaFYYa%lyc^J zW0DIC7Re_spYvU{iWUoc^}{f+Vtqxfdv1x+l#bG%D~E>MJCExClyD`fGlBtJ`aezm zWAFyN?y+nx-}}|ywP{;yHzO!l8>I-Pkl$*yM?@dA%WES56zd>1@MXYXexg5cl~QuMJb8#VV2FOVYQ6 z6a)-ff4+E5FwFmWiirs%!t=9WNXy20|xKokHetE*VCm#Kow!CaM$VxAyfC z^Uq^PD(1UXb7i;Gy%=jHL>*I*$5+rILH1OI4QvFyZS`{cj+O}-IXMwzU(SRpNk#3h{_nW!2&?hr?WnbUb!?U7JrDn;l{R<4Gn>d{YWHx z8-m5RHXPVD;{_a0VPL#Vd=a6*%cVhg(>oZrpb-m$~Ji7=s`CUOLgA zbTRl(@^=|&jT;%=!ub>FXvDS2rPMUOlzl3lhBD#mGHr7W#ijZzc!+ z-Gx@Y-u%d!*7w5~yLY+yHGn{B4}Kv3EFhoTm#Dq#Uv5tvFM`!7t1^$c;h6Q=;i1R1 z0)c@5rgCM^%~lGP*r))Wb|rHIXYRyOkk1`*tv`Sh4Y?*{eUz>d-Y~&T(TG`Cr)|#K zan^za&LNQU&90UlJ+V8A7IX!0r z0tf&w3T_rVn$ zmZ%Cb7{4i&OHh5}fBDl_@YPM3yIV$Ut6r$V2rW`8vP*S!E*c>+QgGSc;s_n%)4gU` zXB)eoQ#+KMEH~7-IoThB3(QG1@IUL!D@_2(V`WpWnKb;|l-b|KmF+bQA0vn&#U2|- znH92{WCe6qdWCi+QXlgw2nnc9;{U@-`0d9yqxxHDYMdK~mAFR!n_NO?B!{INzlX&+ zhjTIWvsI<(>Z9iBPIfu`BZ9Zw+v|8sOgjLZ`vHf*0y*J{Ula>9x;0YGN2m%*;2}MI z4$N9oRNX!-*i6NYl8Pfy{v^8)Z0+u_<*+oDAPXdU7_f2+d%711NgQtbCQ`FbD~8dE zvsQqXK4cLi{n`IwQ1GWrHtjsBZjjVO=CfX=vBF#LfG^sPM_KNbMK9+Cuy;9N$B*^1 zA;hTsddP>%Ve&DUe$6=CUxMq~kO|V!*!*tWU?3X(%e{L1I^?j|H2uee_j?7?e=^D= zbhberh*jv>f$?s?BkSVz+>W3;m7{)Iz%~aBxRLO>->@0R`~GvYmMs~vcTuFD1VK9# z#n+z)uB?n)sdZpV?<%z1W>yUgcls1+?ppC-|F>(+cpyCb0||jYle&T! zLMT1O?*iHq=sLC*CP9$wAI*^EXr+ader3(#qyMG~d`?qCyhHy)i`Cg|8{Ow)6 zSQttXck&d!Is55}lF~c2o(W1J6;e6gkQp2ayVI{HX90>HJByXK*r^iD3kaVe81u`T z2p6CFuqytQUB-Pw*JZpJ?6Rx&N-_+r?}$msRg%d@R5J}zo$AlkXer-P-N90?fa+UV z>`G>vs)JCIdJRJbq3d1E|a)`!Q+#Ob>YkqG_Wh3MGU79Mv)m*e#>5)z?0`?4RmV%)Qp5y>0uE$jez76EMKFSZHNVoK zQP|%DKDMx9LY8Cs>?DBd?ssRiUE;+x}qC0y(z3BevKr^IO@jaG`Ex| zNye&CP65Xa=I0!{@+;h%M z8?FtiMw+N~JKFX74_u}T1Gf+d*a0{h9Uow3GV!!s)nIw%(eGfyp$s3i*H@m$U?zA9 zs`}-zJRI{5DMaBxd=v(>9I|d&m#o^_uF1&xd%JM0VdPtjqiIT3?hYRon>sS_d(tP# z-cH%ST4Gw(Gg^iHqlIo>kxsxwVq{+%dt&U0Z1iKS%vU=rCfN*0!cSE{0&bBNY>-F^ nc?Oy_&JQLNE`z9VO!!RvA+ZezXy|Mde=|=DrO; { + #[test] + fn $name() { + let bytes = include_bytes!($path); + let key = PrivateKey::load(bytes).unwrap(); + assert!(matches!(key, PrivateKey::$kind(_)), "wrong key type"); + } + }; +} + +macro_rules! load_encrypted_test { + ($name:ident, $kind:ident, $path:literal) => { + #[test] + fn $name() { + let bytes = include_bytes!($path); + let key = PrivateKey::load_encrypted(bytes, PASSWORD).unwrap(); + assert!(matches!(key, PrivateKey::$kind(_)), "wrong key type"); + } + }; +} + +load_test!(load_rsa_pkcs1_pem, Rsa, "./keys/rsa.pkcs1.pem"); +load_test!(load_rsa_pkcs1_der, Rsa, "./keys/rsa.pkcs1.der"); +load_test!(load_rsa_pkcs8_pem, Rsa, "./keys/rsa.pkcs8.pem"); +load_test!(load_rsa_pkcs8_der, Rsa, "./keys/rsa.pkcs8.der"); +load_test!(load_ec_p256_sec1_pem, EcP256, "./keys/ec-p256.sec1.pem"); +load_test!(load_ec_p256_sec1_der, EcP256, "./keys/ec-p256.sec1.der"); +load_test!(load_ec_p256_pkcs8_pem, EcP256, "./keys/ec-p256.pkcs8.pem"); +load_test!(load_ec_p256_pkcs8_der, EcP256, "./keys/ec-p256.pkcs8.der"); +load_test!(load_ec_p384_sec1_pem, EcP384, "./keys/ec-p384.sec1.pem"); +load_test!(load_ec_p384_sec1_der, EcP384, "./keys/ec-p384.sec1.der"); +load_test!(load_ec_p384_pkcs8_pem, EcP384, "./keys/ec-p384.pkcs8.pem"); +load_test!(load_ec_p384_pkcs8_der, EcP384, "./keys/ec-p384.pkcs8.der"); +load_test!(load_ec_k256_sec1_pem, EcK256, "./keys/ec-k256.sec1.pem"); +load_test!(load_ec_k256_sec1_der, EcK256, "./keys/ec-k256.sec1.der"); +load_test!(load_ec_k256_pkcs8_pem, EcK256, "./keys/ec-k256.pkcs8.pem"); +load_test!(load_ec_k256_pkcs8_der, EcK256, "./keys/ec-k256.pkcs8.der"); + +load_encrypted_test!( + load_encrypted_rsa_pkcs8_pem, + Rsa, + "./keys/rsa.pkcs8.encrypted.pem" +); +load_encrypted_test!( + load_encrypted_rsa_pkcs8_der, + Rsa, + "./keys/rsa.pkcs8.encrypted.der" +); +load_encrypted_test!( + load_encrypted_ec_p256_pkcs8_pem, + EcP256, + "./keys/ec-p256.pkcs8.encrypted.pem" +); +load_encrypted_test!( + load_encrypted_ec_p256_pkcs8_der, + EcP256, + "./keys/ec-p256.pkcs8.encrypted.der" +); +load_encrypted_test!( + load_encrypted_ec_p384_pkcs8_pem, + EcP384, + "./keys/ec-p384.pkcs8.encrypted.pem" +); +load_encrypted_test!( + load_encrypted_ec_p384_pkcs8_der, + EcP384, + "./keys/ec-p384.pkcs8.encrypted.der" +); +load_encrypted_test!( + load_encrypted_ec_k256_pkcs8_pem, + EcK256, + "./keys/ec-k256.pkcs8.encrypted.pem" +); +load_encrypted_test!( + load_encrypted_ec_k256_pkcs8_der, + EcK256, + "./keys/ec-k256.pkcs8.encrypted.der" +);