You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-31 09:24:31 +03:00
Add a dedicated keystore crate
This commit is contained in:
@ -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,
|
||||
|
@ -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" }
|
||||
|
@ -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<PasswordOrFile>,
|
||||
|
||||
#[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<StaticKeystore> {
|
||||
let mut store = StaticKeystore::new();
|
||||
|
||||
pub async fn key_store(&self) -> anyhow::Result<Keystore> {
|
||||
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
|
||||
|
@ -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"
|
||||
|
@ -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<B>(
|
||||
pool: &PgPool,
|
||||
templates: &Templates,
|
||||
key_store: &Arc<StaticKeystore>,
|
||||
key_store: &Keystore,
|
||||
encrypter: &Encrypter,
|
||||
mailer: &Mailer,
|
||||
url_builder: &UrlBuilder,
|
||||
@ -251,12 +251,8 @@ async fn test_router(pool: &PgPool) -> Result<Router, anyhow::Error> {
|
||||
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]);
|
||||
|
||||
|
@ -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<Arc<StaticKeystore>>,
|
||||
Extension(key_store): Extension<Keystore>,
|
||||
Extension(url_builder): Extension<UrlBuilder>,
|
||||
) -> 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());
|
||||
|
@ -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<Arc<StaticKeystore>>,
|
||||
) -> Result<impl IntoResponse, Infallible> {
|
||||
let jwks = key_store.to_public_jwks();
|
||||
Ok(Json(jwks))
|
||||
pub(crate) async fn get(Extension(key_store): Extension<Keystore>) -> impl IntoResponse {
|
||||
let jwks = key_store.public_jwks();
|
||||
Json(jwks)
|
||||
}
|
||||
|
@ -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<mas_keystore::WrongAlgorithmError> for RouteError {
|
||||
fn from(e: mas_keystore::WrongAlgorithmError) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for RouteError {
|
||||
fn from(e: sqlx::Error) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
@ -182,7 +190,7 @@ impl From<JwtSignatureError> for RouteError {
|
||||
#[tracing::instrument(skip_all, err)]
|
||||
pub(crate) async fn post(
|
||||
client_authorization: ClientAuthorization<AccessTokenRequest>,
|
||||
Extension(key_store): Extension<Arc<StaticKeystore>>,
|
||||
Extension(key_store): Extension<Keystore>,
|
||||
Extension(url_builder): Extension<UrlBuilder>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(encrypter): Extension<Encrypter>,
|
||||
@ -235,7 +243,7 @@ fn hash<H: Digest>(mut hasher: H, token: &str) -> anyhow::Result<String> {
|
||||
async fn authorization_code_grant(
|
||||
grant: &AuthorizationCodeGrant,
|
||||
client: &Client<PostgresqlBackend>,
|
||||
key_store: &StaticKeystore,
|
||||
key_store: &Keystore,
|
||||
url_builder: &UrlBuilder,
|
||||
mut txn: Transaction<'_, Postgres>,
|
||||
) -> Result<AccessTokenResponse, RouteError> {
|
||||
@ -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
|
||||
};
|
||||
|
@ -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<UrlBuilder>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
Extension(key_store): Extension<Arc<StaticKeystore>>,
|
||||
Extension(key_store): Extension<Keystore>,
|
||||
user_authorization: UserAuthorization,
|
||||
) -> Result<Response, FancyError> {
|
||||
// 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())
|
||||
}
|
||||
|
@ -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> {
|
||||
|
@ -104,17 +104,7 @@ impl TryFrom<PrivateJsonWebKey> for PublicJsonWebKey {
|
||||
type Error = SymetricKeyError;
|
||||
|
||||
fn try_from(value: PrivateJsonWebKey) -> Result<Self, Self::Error> {
|
||||
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<P> JsonWebKey<P> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_map<M, O, E>(self, mapper: M) -> Result<JsonWebKey<O>, E>
|
||||
where
|
||||
M: FnOnce(P) -> Result<O, E>,
|
||||
{
|
||||
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<M, O>(self, mapper: M) -> JsonWebKey<O>
|
||||
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<M, O, E>(&self, mapper: M) -> Result<JsonWebKey<O>, E>
|
||||
where
|
||||
M: FnOnce(&P) -> Result<O, E>,
|
||||
{
|
||||
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<M, O>(&self, mapper: M) -> JsonWebKey<O>
|
||||
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<P> {
|
||||
keys: Vec<JsonWebKey<P>>,
|
||||
}
|
||||
|
||||
impl<P> Default for JsonWebKeySet<P> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
keys: Vec::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type PublicJsonWebKeySet = JsonWebKeySet<self::public_parameters::JsonWebKeyPublicParameters>;
|
||||
pub type PrivateJsonWebKeySet =
|
||||
JsonWebKeySet<self::private_parameters::JsonWebKeyPrivateParameters>;
|
||||
@ -229,6 +295,13 @@ impl<P> JsonWebKeySet<P> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> FromIterator<JsonWebKey<P>> for JsonWebKeySet<P> {
|
||||
fn from_iter<T: IntoIterator<Item = JsonWebKey<P>>>(iter: T) -> Self {
|
||||
let keys = iter.into_iter().collect();
|
||||
Self { keys }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -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<u8>,
|
||||
}
|
||||
|
||||
#[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<Self, Self::Err> {
|
||||
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<T> {
|
||||
header: JsonWebSignatureHeader,
|
||||
payload: T,
|
||||
}
|
||||
|
||||
impl<T> DecodedJsonWebToken<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn serialize(&self) -> Result<String, JwtSerializeError> {
|
||||
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<S: SigningKeystore>(
|
||||
&self,
|
||||
store: &S,
|
||||
) -> Result<JsonWebTokenParts, JwtSignatureError> {
|
||||
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<T> DecodedJsonWebToken<T> {
|
||||
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<T> FromStr for DecodedJsonWebToken<T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
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<T: DeserializeOwned>(&self) -> anyhow::Result<DecodedJsonWebToken<T>> {
|
||||
let decoded = self.payload.parse()?;
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
pub fn verify<S: VerifyingKeystore>(
|
||||
&self,
|
||||
header: &JsonWebSignatureHeader,
|
||||
store: &S,
|
||||
) -> S::Future {
|
||||
store.verify(header, self.payload.as_bytes(), &self.signature)
|
||||
}
|
||||
|
||||
pub async fn decode_and_verify<T: DeserializeOwned, S: VerifyingKeystore>(
|
||||
&self,
|
||||
store: &S,
|
||||
) -> anyhow::Result<DecodedJsonWebToken<T>>
|
||||
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},
|
||||
};
|
||||
|
@ -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},
|
||||
};
|
@ -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<String, rsa::RsaPrivateKey>,
|
||||
es256_keys: HashMap<String, SigningKey<NistP256>>,
|
||||
}
|
||||
|
||||
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<NistP256>) -> 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<JsonWebSignatureAlg> {
|
||||
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<JsonWebSignatureHeader> {
|
||||
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<Vec<u8>> {
|
||||
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<Result<(), Self::Error>>;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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<JsonWebSignatureAlg>;
|
||||
|
||||
async fn prepare_header(
|
||||
&self,
|
||||
alg: JsonWebSignatureAlg,
|
||||
) -> anyhow::Result<JsonWebSignatureHeader>;
|
||||
|
||||
async fn sign(&self, header: &JsonWebSignatureHeader, msg: &[u8]) -> anyhow::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub trait VerifyingKeystore {
|
||||
type Error;
|
||||
type Future: Future<Output = Result<(), Self::Error>>;
|
||||
|
||||
fn verify(&self, header: &JsonWebSignatureHeader, msg: &[u8], signature: &[u8])
|
||||
-> Self::Future;
|
||||
}
|
@ -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},
|
||||
};
|
||||
|
@ -36,6 +36,78 @@ pub enum Signer {
|
||||
Es256K { key: jwa::Es256KSigningKey },
|
||||
}
|
||||
|
||||
impl From<jwa::Hs256Key> for Signer {
|
||||
fn from(key: jwa::Hs256Key) -> Self {
|
||||
Self::Hs256 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Hs384Key> for Signer {
|
||||
fn from(key: jwa::Hs384Key) -> Self {
|
||||
Self::Hs384 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Hs512Key> for Signer {
|
||||
fn from(key: jwa::Hs512Key) -> Self {
|
||||
Self::Hs512 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Rs256SigningKey> for Signer {
|
||||
fn from(key: jwa::Rs256SigningKey) -> Self {
|
||||
Self::Rs256 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Rs384SigningKey> for Signer {
|
||||
fn from(key: jwa::Rs384SigningKey) -> Self {
|
||||
Self::Rs384 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Rs512SigningKey> for Signer {
|
||||
fn from(key: jwa::Rs512SigningKey) -> Self {
|
||||
Self::Rs512 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Ps256SigningKey> for Signer {
|
||||
fn from(key: jwa::Ps256SigningKey) -> Self {
|
||||
Self::Ps256 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Ps384SigningKey> for Signer {
|
||||
fn from(key: jwa::Ps384SigningKey) -> Self {
|
||||
Self::Ps384 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Ps512SigningKey> for Signer {
|
||||
fn from(key: jwa::Ps512SigningKey) -> Self {
|
||||
Self::Ps512 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Es256SigningKey> for Signer {
|
||||
fn from(key: jwa::Es256SigningKey) -> Self {
|
||||
Self::Es256 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Es384SigningKey> for Signer {
|
||||
fn from(key: jwa::Es384SigningKey) -> Self {
|
||||
Self::Es384 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Es256KSigningKey> for Signer {
|
||||
fn from(key: jwa::Es256KSigningKey) -> Self {
|
||||
Self::Es256K { key }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SignerFromJwkError {
|
||||
#[error("invalid RSA key")]
|
||||
|
@ -36,6 +36,78 @@ pub enum Verifier {
|
||||
Es256K { key: jwa::Es256KVerifyingKey },
|
||||
}
|
||||
|
||||
impl From<jwa::Hs256Key> for Verifier {
|
||||
fn from(key: jwa::Hs256Key) -> Self {
|
||||
Self::Hs256 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Hs384Key> for Verifier {
|
||||
fn from(key: jwa::Hs384Key) -> Self {
|
||||
Self::Hs384 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Hs512Key> for Verifier {
|
||||
fn from(key: jwa::Hs512Key) -> Self {
|
||||
Self::Hs512 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Rs256VerifyingKey> for Verifier {
|
||||
fn from(key: jwa::Rs256VerifyingKey) -> Self {
|
||||
Self::Rs256 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Rs384VerifyingKey> for Verifier {
|
||||
fn from(key: jwa::Rs384VerifyingKey) -> Self {
|
||||
Self::Rs384 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Rs512VerifyingKey> for Verifier {
|
||||
fn from(key: jwa::Rs512VerifyingKey) -> Self {
|
||||
Self::Rs512 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Ps256VerifyingKey> for Verifier {
|
||||
fn from(key: jwa::Ps256VerifyingKey) -> Self {
|
||||
Self::Ps256 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Ps384VerifyingKey> for Verifier {
|
||||
fn from(key: jwa::Ps384VerifyingKey) -> Self {
|
||||
Self::Ps384 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Ps512VerifyingKey> for Verifier {
|
||||
fn from(key: jwa::Ps512VerifyingKey) -> Self {
|
||||
Self::Ps512 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Es256VerifyingKey> for Verifier {
|
||||
fn from(key: jwa::Es256VerifyingKey) -> Self {
|
||||
Self::Es256 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Es384VerifyingKey> for Verifier {
|
||||
fn from(key: jwa::Es384VerifyingKey) -> Self {
|
||||
Self::Es384 { key }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<jwa::Es256KVerifyingKey> for Verifier {
|
||||
fn from(key: jwa::Es256KVerifyingKey) -> Self {
|
||||
Self::Es256K { key }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum VerifierFromJwkError {
|
||||
#[error("invalid RSA key")]
|
||||
|
@ -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::*;
|
||||
|
||||
|
27
crates/keystore/Cargo.toml
Normal file
27
crates/keystore/Cargo.toml
Normal file
@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "mas-keystore"
|
||||
version = "0.1.0"
|
||||
authors = ["Quentin Gliech <quenting@element.io>"]
|
||||
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" }
|
652
crates/keystore/src/lib.rs
Normal file
652
crates/keystore/src/lib.rs
Normal file
@ -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<LoadError>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A single private key
|
||||
#[non_exhaustive]
|
||||
pub enum PrivateKey {
|
||||
Rsa(Box<rsa::RsaPrivateKey>),
|
||||
EcP256(Box<elliptic_curve::SecretKey<p256::NistP256>>),
|
||||
EcP384(Box<elliptic_curve::SecretKey<p384::NistP384>>),
|
||||
EcK256(Box<elliptic_curve::SecretKey<k256::Secp256k1>>),
|
||||
}
|
||||
|
||||
/// 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<Self, LoadError> {
|
||||
// Taken from `TryFrom<pkcs8::PrivateKeyInfo<'_>> 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<Self, LoadError> {
|
||||
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<Self, LoadError> {
|
||||
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<Zeroizing<Vec<u8>>, 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<Zeroizing<String>, 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<Self, LoadError> {
|
||||
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<Self, LoadError> {
|
||||
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<Self, LoadError> {
|
||||
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<Self, LoadError> {
|
||||
// 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<Self, LoadError> {
|
||||
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<Self, LoadError> {
|
||||
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<Verifier, WrongAlgorithmError> {
|
||||
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<Signer, WrongAlgorithmError> {
|
||||
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<R: RngCore + CryptoRng>(mut rng: R) -> Result<Self, rsa::errors::Error> {
|
||||
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<R: RngCore + CryptoRng>(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<R: RngCore + CryptoRng>(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<R: RngCore + CryptoRng>(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<JsonWebKeySet<PrivateKey>>,
|
||||
}
|
||||
|
||||
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<PrivateKey>) -> 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<PrivateKey>> {
|
||||
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<PrivateKey>> {
|
||||
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<PrivateKey>> {
|
||||
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<JsonWebSignatureAlg> {
|
||||
let mut algs: Vec<_> = self
|
||||
.keys
|
||||
.iter()
|
||||
.flat_map(|key| key.params().possible_algs())
|
||||
.copied()
|
||||
.collect();
|
||||
algs.sort();
|
||||
algs.dedup();
|
||||
algs
|
||||
}
|
||||
}
|
30
crates/keystore/tests/generate.sh
Normal file
30
crates/keystore/tests/generate.sh
Normal file
@ -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"
|
BIN
crates/keystore/tests/keys/ec-k256.pkcs8.der
Normal file
BIN
crates/keystore/tests/keys/ec-k256.pkcs8.der
Normal file
Binary file not shown.
BIN
crates/keystore/tests/keys/ec-k256.pkcs8.encrypted.der
Normal file
BIN
crates/keystore/tests/keys/ec-k256.pkcs8.encrypted.der
Normal file
Binary file not shown.
7
crates/keystore/tests/keys/ec-k256.pkcs8.encrypted.pem
Normal file
7
crates/keystore/tests/keys/ec-k256.pkcs8.encrypted.pem
Normal file
@ -0,0 +1,7 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhwADdSwH2MNgICCAAw
|
||||
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEH8t882jspL2rajw+8VU6TcEgZAb
|
||||
CF6z9u2wMvUdDsj83zLoOyMxC+QB46EGr6EwnZP5zzy3iq+0fAnmufKp16/Lziiy
|
||||
CQecvYb9qZ67NejmrirFP95OLIXm7Sc38aBLJ7DXirUC3msOPyTGOgz2qUwM2xvu
|
||||
zy+aNzsEwRkOD3i1yWipIGSZ++oQ/FSnJ3ALKPjNZlDG86yBBi2FN+Ug2uz2rOE=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
5
crates/keystore/tests/keys/ec-k256.pkcs8.pem
Normal file
5
crates/keystore/tests/keys/ec-k256.pkcs8.pem
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgTzJ5SHXXU2mq7Z7nBfGM
|
||||
L8YDmVsZ7SPQpVepk/Xa+qKhRANCAARIp9S0pouoJ+Er6nfPzvRbn3I5i936SMZZ
|
||||
760rXpKZbNkltEKB0dqQcTkwLq0lGe952xigpOtigO/9dkgEj3OU
|
||||
-----END PRIVATE KEY-----
|
BIN
crates/keystore/tests/keys/ec-k256.sec1.der
Normal file
BIN
crates/keystore/tests/keys/ec-k256.sec1.der
Normal file
Binary file not shown.
5
crates/keystore/tests/keys/ec-k256.sec1.pem
Normal file
5
crates/keystore/tests/keys/ec-k256.sec1.pem
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHQCAQEEIE8yeUh111Npqu2e5wXxjC/GA5lbGe0j0KVXqZP12vqioAcGBSuBBAAK
|
||||
oUQDQgAESKfUtKaLqCfhK+p3z870W59yOYvd+kjGWe+tK16SmWzZJbRCgdHakHE5
|
||||
MC6tJRnvedsYoKTrYoDv/XZIBI9zlA==
|
||||
-----END EC PRIVATE KEY-----
|
BIN
crates/keystore/tests/keys/ec-p256.pkcs8.der
Normal file
BIN
crates/keystore/tests/keys/ec-p256.pkcs8.der
Normal file
Binary file not shown.
BIN
crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.der
Normal file
BIN
crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.der
Normal file
Binary file not shown.
7
crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.pem
Normal file
7
crates/keystore/tests/keys/ec-p256.pkcs8.encrypted.pem
Normal file
@ -0,0 +1,7 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhIOTdQ9pS7EgICCAAw
|
||||
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEEVvTsSwG1HEr6urEKUSC8kEgZCQ
|
||||
fLQHNDHSjGin9RvcMYi5htsKZbRJK1JL19o7cf8W4AH0kKNAlDtJBrc7j/9tlCkP
|
||||
b/7O7KFCNkeCrfF113mzgoRuD4xLzoe3n+ybpeBgf8WJuJowiZwhKGXGlUP/m+XX
|
||||
aWiCKUaaA4huhJbQzJDBdVUnKEZZ+lysEMjYjNgplGc2uvoNSywWKHubgY9Wj0Y=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
5
crates/keystore/tests/keys/ec-p256.pkcs8.pem
Normal file
5
crates/keystore/tests/keys/ec-p256.pkcs8.pem
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5Ru1AmWbX0F4p3X0
|
||||
8YIWMnVm+6KJqQiIjm0Pw2BDqO6hRANCAARQQd/kCEAv7PYjKvA+xhQAvnQXNbXZ
|
||||
fXfUHEiuBjpV2b70TZCr08POfUZf/BjTHG+NuluyLFle6dJWIga1muhV
|
||||
-----END PRIVATE KEY-----
|
BIN
crates/keystore/tests/keys/ec-p256.sec1.der
Normal file
BIN
crates/keystore/tests/keys/ec-p256.sec1.der
Normal file
Binary file not shown.
5
crates/keystore/tests/keys/ec-p256.sec1.pem
Normal file
5
crates/keystore/tests/keys/ec-p256.sec1.pem
Normal file
@ -0,0 +1,5 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MHcCAQEEIOUbtQJlm19BeKd19PGCFjJ1ZvuiiakIiI5tD8NgQ6juoAoGCCqGSM49
|
||||
AwEHoUQDQgAEUEHf5AhAL+z2IyrwPsYUAL50FzW12X131BxIrgY6Vdm+9E2Qq9PD
|
||||
zn1GX/wY0xxvjbpbsixZXunSViIGtZroVQ==
|
||||
-----END EC PRIVATE KEY-----
|
BIN
crates/keystore/tests/keys/ec-p384.pkcs8.der
Normal file
BIN
crates/keystore/tests/keys/ec-p384.pkcs8.der
Normal file
Binary file not shown.
BIN
crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.der
Normal file
BIN
crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.der
Normal file
Binary file not shown.
8
crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.pem
Normal file
8
crates/keystore/tests/keys/ec-p384.pkcs8.encrypted.pem
Normal file
@ -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-----
|
6
crates/keystore/tests/keys/ec-p384.pkcs8.pem
Normal file
6
crates/keystore/tests/keys/ec-p384.pkcs8.pem
Normal file
@ -0,0 +1,6 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDB7jIEkTYf/O4QMPM+o
|
||||
KGha8Z9rgfRVOJNWMHRuLvw5HuIJhgobe9n1xUaydBj7/nmhZANiAASsmR291tkF
|
||||
a+aXcNUqBec55lIFYjRvaKvOP7vL7nSj1PGsPgud/YF7w33yb56fwE7H9ELG3+3j
|
||||
JM26/rx9JZyKurTnhQVkWe/ZHB+59Dqke9zAzDXW0vRmOoFCrZL8IRw=
|
||||
-----END PRIVATE KEY-----
|
BIN
crates/keystore/tests/keys/ec-p384.sec1.der
Normal file
BIN
crates/keystore/tests/keys/ec-p384.sec1.der
Normal file
Binary file not shown.
6
crates/keystore/tests/keys/ec-p384.sec1.pem
Normal file
6
crates/keystore/tests/keys/ec-p384.sec1.pem
Normal file
@ -0,0 +1,6 @@
|
||||
-----BEGIN EC PRIVATE KEY-----
|
||||
MIGkAgEBBDB7jIEkTYf/O4QMPM+oKGha8Z9rgfRVOJNWMHRuLvw5HuIJhgobe9n1
|
||||
xUaydBj7/nmgBwYFK4EEACKhZANiAASsmR291tkFa+aXcNUqBec55lIFYjRvaKvO
|
||||
P7vL7nSj1PGsPgud/YF7w33yb56fwE7H9ELG3+3jJM26/rx9JZyKurTnhQVkWe/Z
|
||||
HB+59Dqke9zAzDXW0vRmOoFCrZL8IRw=
|
||||
-----END EC PRIVATE KEY-----
|
7
crates/keystore/tests/keys/ec256.pkcs8.encrypted.pem
Normal file
7
crates/keystore/tests/keys/ec256.pkcs8.encrypted.pem
Normal file
@ -0,0 +1,7 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIHsMFcGCSqGSIb3DQEFDTBKMCkGCSqGSIb3DQEFDDAcBAhuoY9YhPfgvwICCAAw
|
||||
DAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEIYpzmbQGScgbLLC3aqyxIAEgZB0
|
||||
BfhaPQtEGidyNscFjopgxP/wHTdrvzFzqgikeOif9/GeaEXjRlc6vBEGnkq0gR6P
|
||||
i1E1ie31wwasBK3EwvvSgJdMsSQyD+RjpQy+0RqncNhsJvE9gshMCSWDxqR/CIJw
|
||||
VnZeGxhWFYQf9ybcBBwp2W/bqInPdQdqGwUi9agkWdmui2B9bb4eKz5p2htVClQ=
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
BIN
crates/keystore/tests/keys/rsa.pkcs1.der
Normal file
BIN
crates/keystore/tests/keys/rsa.pkcs1.der
Normal file
Binary file not shown.
27
crates/keystore/tests/keys/rsa.pkcs1.pem
Normal file
27
crates/keystore/tests/keys/rsa.pkcs1.pem
Normal file
@ -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-----
|
BIN
crates/keystore/tests/keys/rsa.pkcs8.der
Normal file
BIN
crates/keystore/tests/keys/rsa.pkcs8.der
Normal file
Binary file not shown.
BIN
crates/keystore/tests/keys/rsa.pkcs8.encrypted.der
Normal file
BIN
crates/keystore/tests/keys/rsa.pkcs8.encrypted.der
Normal file
Binary file not shown.
30
crates/keystore/tests/keys/rsa.pkcs8.encrypted.pem
Normal file
30
crates/keystore/tests/keys/rsa.pkcs8.encrypted.pem
Normal file
@ -0,0 +1,30 @@
|
||||
-----BEGIN ENCRYPTED PRIVATE KEY-----
|
||||
MIIFLTBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQIldrvrDo5PWcCAggA
|
||||
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBquprEJE7GFuU0u3tNY2ClBIIE
|
||||
0JMYmOQrvzJpSCGu41M3iT9vJ8rk+u286cG4Ly8ARrPTIhn/Nr74hd5Uh0ezmc7t
|
||||
DutPAliWqAoxGL8plktQfeR/+dNX8NVNmbtbRmurXqjBCC1SK7JLdXUuGiEyUnDZ
|
||||
3uIIHePTvWZYZxu7nlM7lJ0sN9S5OdkfgwvMSTXwP+vMK00AymNlU66ZNPzclNnf
|
||||
xZXtNQwfI4gixR4bSJkVsppkBXRWeoV2JVIf8yoICahP4v/DS/jtxzij+ltoapwb
|
||||
h5W4Rv6bXEJnJIelG7+HxTmHczHzs1Wl6eqj5upOdjn8I4K33NvreEtMOwCb+wGH
|
||||
wAI54YlfFAs1ujVSzUeoCkvgIT19DsxaUlTdPizYTMWMkoojUf4kuxBs7e6w5bA0
|
||||
lyALJHkYsHILe7oXCUkcbvsmUrNOK4bRirNWgJY+ZBEj/dn/YlDwp0w27TyqbpDU
|
||||
ROazXqaxwlvvgC/ajkdZ7pVFL+cFsHc5YMeP9bMhxKj8esiHy2HyMZh+zTjcX4Lr
|
||||
25AJ0tHY/G7bYCAa/cu9evw65LDXNP3Hg/d7BdN/MsojdLZBHohV6NmB8W6/I+vi
|
||||
9dyOoDyVo35awDYHDbiR8LrfsBnG+/5zs5YtGbF3UaSfBEE7FMXXnweurIxcqXVg
|
||||
Su8qtNxmVg0Ld/oUau5a8WH/tyV27CHHM+T1XQ54IDz6AwmBP5O6ghhCKD3F7wLa
|
||||
Eug6yjug9G/mRQjXRQ1qdD/tA8D95a0uAZO0kYRE/2WsoF8lkzVJLPxCTENnLNL8
|
||||
uSkfLRk2q92JtzZJKk4kbSo6kIh4p3tykqULDXa7fyF1+7dleIZwS5T5XqcI/O1d
|
||||
vVgYShF38hS/OfvpiZLSO7aeCUohFVI53pAZHAm7068nZwGKHjuLlbfYqRLNCwif
|
||||
IBjzy5oIF5/6sCr+lV3GfUPXZbwZ7LKr9koyDKzviEnJVZLJRlQzDVOd6s/VaCnf
|
||||
Ut3BUq+A6ltY7Epmm6qDUJN6EmhXWlMRf+AcKh01p3VyvenwKrbDjj1fmBJ3Se7V
|
||||
o0Xp0+hnYX5V+S/fzGa2VC9UuC/GeWM8YNaHx8rE6bsNkQj93YXo2NYWHndDl+P1
|
||||
LxI+iALWjC6O+anpbT4/VOSh/YM25qUK3PSn7RIqBpSCpqxQfdF715tuccDKkzbp
|
||||
cc/a72FXr/9iONWhNcQvzwIVTHMq5LK0pgBCufhyCpNvpzTaHBKIfZkjr4U67IBP
|
||||
nFXHS9tSxASFAg5kY36xIbxFMIKANb/SoVHY3wM+trBjQpZj+ewklW9s4zdWRdGj
|
||||
UHwma2KKAZF+uqIbMr0prCJ+jSTIOOrG5DS3lCdJxqpRTgHHDebnIEQHxJf6/FzI
|
||||
s3Q09YL+huDPG1DttN1tz2TorR+ZV9SL4oJXAMjcc85MG64NqkaaqHU72vX6D1ym
|
||||
CwO3EAfYATgZHR9gZjLxtF3VYHnm0e9gxKEMH7TXV57HYCZ4Cqr65bE8HPMOKUTh
|
||||
QXwUBrQcsm5al6za2q7JyPl7u3CtYeRbi6NpSlbuDh8Wmzoy8XvSJ8neTtm/WmJa
|
||||
1jNahf2jhW5ekU7ARGJkXxt7YuyKbPRjrMxXOysm2QxJwk9VPwJukRVskEgJeQaa
|
||||
Nc4PJhMug6hvTPhM/CGxDQho6GwUfzNPC6XQZ0IU60Zb
|
||||
-----END ENCRYPTED PRIVATE KEY-----
|
28
crates/keystore/tests/keys/rsa.pkcs8.pem
Normal file
28
crates/keystore/tests/keys/rsa.pkcs8.pem
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC5/bzM9SnnviMN
|
||||
F1fq43R3ueJkIhSkAIWj4+j/XcW4gayunN7IxfoJhZU9qWBdb0UKuHEv7i2glHLm
|
||||
emOSCwsWyScvn/NdtYoWC3r1wzCxYn1FrntuidKUjtKgK4eG3DuP6wCUcCWpM4IB
|
||||
XPo/Tf1j8Aa87rFsLt/71d+1aVtsNyMoVxulCKWQ32q2R0QftMtrIwAU6yA28GXA
|
||||
X36iP3Dk8iTe95FN0FcjgaUeOGHOoZkqJGmg/0efJwce97JP4Lfe+6FCyjlrfBxQ
|
||||
268NU6LFKpHiS9K3hRVY0r8qYRCaVZTX+b0KUjCuzxDv2rTGQZpJrSK4brSe+bu1
|
||||
l7P1MWTHAgMBAAECggEAcDliSmlDt8lKbQPA0MId7IAE0laeDbci7BkA5dU3EHVu
|
||||
NI4C/zI2BLjNrD8fHkGdbzKLWVhlv5bTHa6LlfuD3WPSeTISLmDzyQl0UAZOwzmi
|
||||
TQFTCQewQGNNQ8gAJqeMaPJrjyV8E/Q4ZYoslTYBxuN1rVp5Q8Wk8NfD/DpuJ4S2
|
||||
FDCuircMcIyeTqL1uzGAg5d43TiKP1cGawco8I/aEBSdlKXHl95th8crv9b3lomn
|
||||
bapr/94dLxwpBkInT51C5nAuGUaixMWotSaqE+W39vUS889jRyrmu1RzZbfUvRhr
|
||||
JUQdU5DHV9AiQfZUhQ1sBL5t9XL6jpYJGTqzVcOGwQKBgQDfS3KRVM3kudxjh5Aj
|
||||
DDaZby/WPUQbqeBVKE3ca4FO5ddoQ4HwO5zJPvIm3VmGZonAyThqCZnX9/9lcG18
|
||||
tEQZXNW+7CpFn4sduojKGLTBAkDRp7M+i165NCcWf4i94ca2lA0NgYv9SCR7G4LF
|
||||
tzYc5kbnUmm6cIrRN21V87/05QKBgQDVO5LZYBfM251EbfF3/g6C2QsP0C99K/KP
|
||||
DYuTCoIxvcDlQSrDu+5uT/GCdG+qsCAlFaiOd2BGtGeKQcJq8McE7QYA7+TOsVF/
|
||||
D576viNwuXRhp2R7yd0JztVOfHjBAKsyLkfscR5HBpk42iPk8A/IDlf43+BPQ00M
|
||||
DsqhBPjkOwKBgG35BZroDmNo2k82k9l/CWD17rRwBhwtxSuLpxU46T2HW1ihbtx3
|
||||
L5e55Z8Ygx/7XjrRn3Qx+E/ydxlpjrLNQYDQRQE75PHXSrfAh+Ao3NT2HCBlnJTg
|
||||
JS3HrryY4SrLiIehUPBm7iA3KKhs+zIPMpp3G1KQNzw4c1XpG3Mqkk61AoGAWhEZ
|
||||
zBucsfkpipft4MM9GePXmkxTYsnHYMlV5TwRFjWSXsOR7q9Y3NuDpZjM+gn+6esm
|
||||
EVc3IWZePbXdCxK83cXIPWkW+n9HbyYH/t2FVr3e/MiZ1vfDF7t3ufk1gEBqD34g
|
||||
/ywgn9uXqL3rX25PHC+C1SqrMo+44Zj12eGhx7UCgYEApnJlz81WCpXYqACediVz
|
||||
A2fuxFKQzx1yrT+AEw25JmR9lK4R3jDBTNGIWVinbc7acdaCBOTOKa4E+0PsqriM
|
||||
F7LdmTIbJ76wB2acqQ6M0gs1ukhmVPqArfN8FoWYkZEmKcoD4El4fv1YWlg3pIFn
|
||||
9zezkG6wBsRTuQTl4ifJrM8=
|
||||
-----END PRIVATE KEY-----
|
97
crates/keystore/tests/load.rs
Normal file
97
crates/keystore/tests/load.rs
Normal file
@ -0,0 +1,97 @@
|
||||
// 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 mas_keystore::PrivateKey;
|
||||
|
||||
static PASSWORD: &str = "hunter2";
|
||||
|
||||
macro_rules! load_test {
|
||||
($name:ident, $kind:ident, $path:literal) => {
|
||||
#[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"
|
||||
);
|
Reference in New Issue
Block a user