1
0
mirror of https://github.com/matrix-org/matrix-authentication-service.git synced 2025-07-29 22:01:14 +03:00

New JWT/JOSE crate

Still WIP, needs to handle time related claims
This commit is contained in:
Quentin Gliech
2022-01-04 22:28:00 +01:00
parent 694a0bff03
commit f933ace007
20 changed files with 1942 additions and 726 deletions

530
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@
use std::{ use std::{
net::{SocketAddr, TcpListener}, net::{SocketAddr, TcpListener},
sync::Arc,
time::Duration, time::Duration,
}; };
@ -236,6 +237,14 @@ impl ServerCommand {
queue.recuring(Duration::from_secs(15), mas_tasks::cleanup_expired(&pool)); queue.recuring(Duration::from_secs(15), mas_tasks::cleanup_expired(&pool));
queue.start(); queue.start();
// Initialize the key store
let key_store = config
.oauth2
.key_store()
.context("could not import keys from config")?;
// Wrap the key store in an Arc
let key_store = Arc::new(key_store);
// Load and compile the templates // Load and compile the templates
let templates = Templates::load_from_config(&config.templates) let templates = Templates::load_from_config(&config.templates)
.await .await
@ -254,7 +263,7 @@ impl ServerCommand {
} }
// Start the server // Start the server
let root = mas_handlers::root(&pool, &templates, &config); let root = mas_handlers::root(&pool, &templates, &key_store, &config);
let warp_service = warp::service(root); let warp_service = warp::service(root);

View File

@ -24,10 +24,11 @@ serde_json = "1.0.72"
sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres"] } sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres"] }
rand = "0.8.4" rand = "0.8.4"
rsa = "0.5.0" rsa = { git = "https://github.com/sandhose/rsa.git", branch = "bump-pkcs" }
k256 = "0.9.6" p256 = { version = "0.10.0", features = ["ecdsa", "pem", "pkcs8"] }
pkcs8 = { version = "0.7.6", features = ["pem"] } pkcs8 = { version = "0.8.0", features = ["pem"] }
elliptic-curve = { version = "0.10.6", features = ["pem"] } elliptic-curve = { version = "0.11.6", features = ["pem", "pkcs8"] }
jwt-compact = { version = "0.5.0-beta.1", features = ["with_rsa", "k256"] }
indoc = "1.0.3" indoc = "1.0.3"
mas-jose = { path = "../jose" }

View File

@ -38,7 +38,7 @@ pub use self::{
csrf::CsrfConfig, csrf::CsrfConfig,
database::DatabaseConfig, database::DatabaseConfig,
http::HttpConfig, http::HttpConfig,
oauth2::{Algorithm, KeySet, OAuth2ClientConfig, OAuth2Config}, oauth2::{OAuth2ClientConfig, OAuth2Config},
telemetry::{ telemetry::{
MetricsConfig, MetricsExporterConfig, Propagator, TelemetryConfig, TracingConfig, MetricsConfig, MetricsExporterConfig, Propagator, TelemetryConfig, TracingConfig,
TracingExporterConfig, TracingExporterConfig,

View File

@ -14,19 +14,14 @@
use anyhow::Context; use anyhow::Context;
use async_trait::async_trait; use async_trait::async_trait;
use jwt_compact::{ use mas_jose::StaticKeystore;
alg::{self, StrongAlg, StrongKey}, use pkcs8::{DecodePrivateKey, EncodePrivateKey};
jwk::JsonWebKey, use rsa::{
AlgorithmExt, Claims, Header, pkcs1::{DecodeRsaPrivateKey, EncodeRsaPrivateKey},
RsaPrivateKey,
}; };
use pkcs8::{FromPrivateKey, ToPrivateKey};
use rsa::RsaPrivateKey;
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{ use serde::{Deserialize, Serialize};
de::{MapAccess, Visitor},
ser::SerializeStruct,
Deserialize, Serialize,
};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use thiserror::Error; use thiserror::Error;
use tokio::task; use tokio::task;
@ -35,257 +30,17 @@ use url::Url;
use super::ConfigurationSection; use super::ConfigurationSection;
// TODO: a lot of the signing logic should go out somewhere else #[derive(JsonSchema, Serialize, Deserialize, Clone, Copy, Debug)]
#[serde(rename_all = "lowercase")]
const RS256: StrongAlg<alg::Rsa> = StrongAlg(alg::Rsa::rs256()); pub enum KeyType {
Rsa,
#[derive(Serialize, Deserialize, Clone, Copy, Debug)] Ecdsa,
#[serde(rename_all = "UPPERCASE")]
pub enum Algorithm {
Rs256,
Es256k,
} }
#[derive(Serialize, Clone)] #[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
pub struct Jwk { pub struct KeyConfig {
kid: String, r#type: KeyType,
alg: Algorithm, key: String,
#[serde(flatten)]
inner: serde_json::Value,
}
#[derive(Serialize, Clone)]
pub struct Jwks {
keys: Vec<Jwk>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(transparent)]
pub struct KeySet(Vec<Key>);
impl KeySet {
#[must_use]
pub fn to_public_jwks(&self) -> Jwks {
let keys = self.0.iter().map(Key::to_public_jwk).collect();
Jwks { keys }
}
#[tracing::instrument(err)]
pub async fn token<T>(
&self,
alg: Algorithm,
header: Header,
claims: Claims<T>,
) -> anyhow::Result<String>
where
T: std::fmt::Debug + Serialize + Send + Sync + 'static,
{
match alg {
Algorithm::Rs256 => {
let (kid, key) = self
.0
.iter()
.find_map(Key::rsa)
.context("could not find RSA key")?;
let header = header.with_key_id(kid);
// TODO: store them as strong keys
let key = StrongKey::try_from(key.clone())?;
task::spawn_blocking(move || {
RS256
.token(header, &claims, &key)
.context("failed to sign token")
})
.await?
}
Algorithm::Es256k => {
// TODO: make this const with lazy_static?
let es256k: alg::Es256k = alg::Es256k::default();
let (kid, key) = self
.0
.iter()
.find_map(Key::ecdsa)
.context("could not find ECDSA key")?;
let key = k256::ecdsa::SigningKey::from(key);
let header = header.with_key_id(kid);
// TODO: use StrongAlg
task::spawn_blocking(move || {
es256k
.token(header, &claims, &key)
.context("failed to sign token")
})
.await?
}
}
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Key {
Rsa {
key: Box<RsaPrivateKey>,
kid: String,
},
Ecdsa {
key: k256::SecretKey,
kid: String,
},
}
impl Key {
fn from_ecdsa(key: k256::SecretKey) -> Self {
// TODO: hash the key and use as KID
let kid = String::from("ecdsa-kid");
Self::Ecdsa { kid, key }
}
fn from_ecdsa_pem(key: &str) -> anyhow::Result<Self> {
let key = k256::SecretKey::from_pkcs8_pem(key)?;
Ok(Self::from_ecdsa(key))
}
fn from_rsa(key: RsaPrivateKey) -> Self {
// TODO: hash the key and use as KID
let kid = String::from("rsa-kid");
Self::Rsa {
kid,
key: Box::new(key),
}
}
fn from_rsa_pem(key: &str) -> anyhow::Result<Self> {
let key = RsaPrivateKey::from_pkcs8_pem(key)?;
Ok(Self::from_rsa(key))
}
fn to_public_jwk(&self) -> Jwk {
match self {
Key::Rsa { key, kid } => {
let pubkey = key.to_public_key();
let inner = JsonWebKey::from(&pubkey);
let inner = serde_json::to_value(&inner).unwrap();
let kid = kid.to_string();
let alg = Algorithm::Rs256;
Jwk { kid, alg, inner }
}
Key::Ecdsa { key, kid } => {
let pubkey = k256::ecdsa::VerifyingKey::from(key.public_key());
let inner = JsonWebKey::from(&pubkey);
let inner = serde_json::to_value(&inner).unwrap();
let kid = kid.to_string();
let alg = Algorithm::Es256k;
Jwk { kid, alg, inner }
}
}
}
fn rsa(&self) -> Option<(&str, &RsaPrivateKey)> {
match self {
Key::Rsa { key, kid } => Some((kid, key)),
_ => None,
}
}
fn ecdsa(&self) -> Option<(&str, &k256::SecretKey)> {
match self {
Key::Ecdsa { key, kid } => Some((kid, key)),
_ => None,
}
}
}
impl Serialize for Key {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut map = serializer.serialize_struct("Key", 2)?;
match self {
Key::Rsa { key, kid: _ } => {
map.serialize_field("type", "rsa")?;
let pem = key.to_pkcs8_pem().map_err(serde::ser::Error::custom)?;
map.serialize_field("key", pem.as_str())?;
}
Key::Ecdsa { key, kid: _ } => {
map.serialize_field("type", "ecdsa")?;
let pem = key.to_pkcs8_pem().map_err(serde::ser::Error::custom)?;
map.serialize_field("key", pem.as_str())?;
}
}
map.end()
}
}
impl<'de> Deserialize<'de> for Key {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize, Debug)]
#[serde(field_identifier, rename_all = "lowercase")]
enum Field {
Type,
Key,
}
#[derive(Deserialize)]
#[serde(rename_all = "lowercase")]
enum KeyType {
Rsa,
Ecdsa,
}
struct KeyVisitor;
impl<'de> Visitor<'de> for KeyVisitor {
type Value = Key;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("struct Key")
}
fn visit_map<V>(self, mut map: V) -> Result<Key, V::Error>
where
V: MapAccess<'de>,
{
let mut key_type = None;
let mut key_key = None;
while let Some(key) = map.next_key()? {
match key {
Field::Type => {
if key_type.is_some() {
return Err(serde::de::Error::duplicate_field("type"));
}
key_type = Some(map.next_value()?);
}
Field::Key => {
if key_key.is_some() {
return Err(serde::de::Error::duplicate_field("key"));
}
key_key = Some(map.next_value()?);
}
}
}
let key_type: KeyType =
key_type.ok_or_else(|| serde::de::Error::missing_field("type"))?;
let key_key: String =
key_key.ok_or_else(|| serde::de::Error::missing_field("key"))?;
match key_type {
KeyType::Rsa => Key::from_rsa_pem(&key_key).map_err(serde::de::Error::custom),
KeyType::Ecdsa => {
Key::from_ecdsa_pem(&key_key).map_err(serde::de::Error::custom)
}
}
}
}
deserializer.deserialize_struct("Key", &["type", "key"], KeyVisitor)
}
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -339,8 +94,8 @@ pub struct OAuth2Config {
#[serde(default)] #[serde(default)]
pub clients: Vec<OAuth2ClientConfig>, pub clients: Vec<OAuth2ClientConfig>,
#[schemars(with = "Vec<String>")] // TODO: this is a lie #[serde(default)]
pub keys: KeySet, pub keys: Vec<KeyConfig>,
} }
impl OAuth2Config { impl OAuth2Config {
@ -350,6 +105,25 @@ impl OAuth2Config {
.join(".well-known/openid-configuration") .join(".well-known/openid-configuration")
.expect("could not build discovery url") .expect("could not build discovery url")
} }
pub fn key_store(&self) -> anyhow::Result<StaticKeystore> {
let mut store = StaticKeystore::new();
for key in &self.keys {
match key.r#type {
KeyType::Ecdsa => {
let key = p256::SecretKey::from_pkcs8_pem(&key.key)?;
store.add_ecdsa_key(key.into())?;
}
KeyType::Rsa => {
let key = rsa::RsaPrivateKey::from_pkcs1_pem(&key.key)?;
store.add_rsa_key(key)?;
}
}
}
Ok(store)
}
} }
#[async_trait] #[async_trait]
@ -373,52 +147,66 @@ impl ConfigurationSection<'_> for OAuth2Config {
}) })
.await .await
.context("could not join blocking task")??; .context("could not join blocking task")??;
let rsa_key = KeyConfig {
r#type: KeyType::Rsa,
key: rsa_key.to_pkcs1_pem(pkcs8::LineEnding::LF)?.to_string(),
};
let span = tracing::info_span!("ecdsa"); let span = tracing::info_span!("ecdsa");
let ecdsa_key = task::spawn_blocking(move || { let ecdsa_key = task::spawn_blocking(move || {
let _entered = span.enter(); let _entered = span.enter();
let rng = rand::thread_rng(); let rng = rand::thread_rng();
let ret = k256::SecretKey::random(rng); let ret = p256::SecretKey::random(rng);
info!("Done generating ECDSA key"); info!("Done generating ECDSA key");
ret ret
}) })
.await .await
.context("could not join blocking task")?; .context("could not join blocking task")?;
let ecdsa_key = KeyConfig {
r#type: KeyType::Ecdsa,
key: ecdsa_key.to_pkcs8_pem(pkcs8::LineEnding::LF)?.to_string(),
};
Ok(Self { Ok(Self {
issuer: default_oauth2_issuer(), issuer: default_oauth2_issuer(),
clients: Vec::new(), clients: Vec::new(),
keys: KeySet(vec![Key::from_rsa(rsa_key), Key::from_ecdsa(ecdsa_key)]), keys: vec![rsa_key, ecdsa_key],
}) })
} }
fn test() -> Self { fn test() -> Self {
let rsa_key = Key::from_rsa_pem(indoc::indoc! {r#" let rsa_key = KeyConfig {
-----BEGIN PRIVATE KEY----- r#type: KeyType::Rsa,
MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN key: indoc::indoc! {r#"
QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU -----BEGIN PRIVATE KEY-----
scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAymS2RkeIZo7pUeEN
3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE QUGCG4GLJru5jzxomO9jiNr5D/oRcerhpQVc9aCpBfAAg4l4a1SmYdBzWqX0X5pU
vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw scgTtQIDAQABAkEArNIMlrxUK4bSklkCcXtXdtdKE9vuWfGyOw0GyAB69fkEUBxh
N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622 3j65u+u3ZmW+bpMWHgp1FtdobE9nGwb2VBTWAQIhAOyU1jiUEkrwKK004+6b5QRE
tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl vC9UI2vDWy5vioMNx5Y1AiEA2wGAJ6ETF8FF2Vd+kZlkKK7J0em9cl0gbJDsWIEw
Gh7BNzCeN+D6 N4ECIEyWYkMurD1WQdTQqnk0Po+DMOihdFYOiBYgRdbnPxWBAiEAmtd0xJAd7622
-----END PRIVATE KEY----- tPQniMnrBtiN2NxqFXHCev/8Gpc8gAECIBcaPcF59qVeRmYrfqzKBxFm7LmTwlAl
"#}) Gh7BNzCeN+D6
.unwrap(); -----END PRIVATE KEY-----
let ecdsa_key = Key::from_ecdsa_pem(indoc::indoc! {r#" "#}
-----BEGIN PRIVATE KEY----- .to_string(),
MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA };
NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn let ecdsa_key = KeyConfig {
OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC r#type: KeyType::Ecdsa,
-----END PRIVATE KEY----- key: indoc::indoc! {r#"
"#}) -----BEGIN PRIVATE KEY-----
.unwrap(); MIGEAgEAMBAGByqGSM49AgEGBSuBBAAKBG0wawIBAQQgqfn5mYO/5Qq/wOOiWgHA
NaiDiepgUJ2GI5eq2V8D8nahRANCAARMK9aKUd/H28qaU+0qvS6bSJItzAge1VHn
OhBAAUVci1RpmUA+KdCL5sw9nadAEiONeiGr+28RYHZmlB9qXnjC
-----END PRIVATE KEY-----
"#}
.to_string(),
};
Self { Self {
issuer: default_oauth2_issuer(), issuer: default_oauth2_issuer(),
clients: Vec::new(), clients: Vec::new(),
keys: KeySet(vec![rsa_key, ecdsa_key]), keys: vec![rsa_key, ecdsa_key],
} }
} }
} }

View File

@ -36,8 +36,12 @@ serde_urlencoded = "0.7.0"
argon2 = { version = "0.3.2", features = ["password-hash"] } argon2 = { version = "0.3.2", features = ["password-hash"] }
# Crypto, hashing and signing stuff # Crypto, hashing and signing stuff
sha2 = "0.10.0" rsa = { git = "https://github.com/sandhose/rsa.git", branch = "bump-pkcs" }
jwt-compact = { version = "0.5.0-beta.1", features = ["with_rsa", "k256"] } pkcs8 = { version = "0.8.0", features = ["pem"] }
elliptic-curve = { version = "0.11.6", features = ["pem"] }
chacha20poly1305 = { version = "0.9.0", features = ["std"] }
sha2 = "0.9.8"
crc = "2.1.0"
# Various data types and utilities # Various data types and utilities
data-encoding = "2.3.2" data-encoding = "2.3.2"
@ -54,3 +58,7 @@ mas-templates = { path = "../templates" }
mas-static-files = { path = "../static-files" } mas-static-files = { path = "../static-files" }
mas-storage = { path = "../storage" } mas-storage = { path = "../storage" }
mas-warp-utils = { path = "../warp-utils" } mas-warp-utils = { path = "../warp-utils" }
mas-jose = { path = "../jose" }
[dev-dependencies]
indoc = "1.0.3"

View File

@ -22,7 +22,10 @@
#![allow(clippy::implicit_hasher)] #![allow(clippy::implicit_hasher)]
#![allow(clippy::unused_async)] // Some warp filters need that #![allow(clippy::unused_async)] // Some warp filters need that
use std::sync::Arc;
use mas_config::RootConfig; use mas_config::RootConfig;
use mas_jose::StaticKeystore;
use mas_static_files::filter as static_files; use mas_static_files::filter as static_files;
use mas_templates::Templates; use mas_templates::Templates;
use sqlx::PgPool; use sqlx::PgPool;
@ -38,10 +41,11 @@ use self::{health::filter as health, oauth2::filter as oauth2, views::filter as
pub fn root( pub fn root(
pool: &PgPool, pool: &PgPool,
templates: &Templates, templates: &Templates,
key_store: &Arc<StaticKeystore>,
config: &RootConfig, config: &RootConfig,
) -> BoxedFilter<(impl Reply,)> { ) -> BoxedFilter<(impl Reply,)> {
let health = health(pool); let health = health(pool);
let oauth2 = oauth2(pool, templates, &config.oauth2, &config.cookies); let oauth2 = oauth2(pool, templates, key_store, &config.oauth2, &config.cookies);
let views = views( let views = views(
pool, pool,
templates, templates,

View File

@ -12,16 +12,20 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use mas_config::OAuth2Config; use std::sync::Arc;
use warp::{filters::BoxedFilter, Filter, Reply};
pub(super) fn filter(config: &OAuth2Config) -> BoxedFilter<(Box<dyn Reply>,)> { use mas_jose::{ExportJwks, StaticKeystore};
let jwks = config.keys.to_public_jwks(); use warp::{filters::BoxedFilter, Filter, Rejection, Reply};
pub(super) fn filter(key_store: &Arc<StaticKeystore>) -> BoxedFilter<(Box<dyn Reply>,)> {
let key_store = key_store.clone();
warp::path!("oauth2" / "keys.json") warp::path!("oauth2" / "keys.json")
.and(warp::get().map(move || { .and(warp::get().map(move || key_store.clone()).and_then(get))
let ret: Box<dyn Reply> = Box::new(warp::reply::json(&jwks));
ret
}))
.boxed() .boxed()
} }
async fn get(key_store: Arc<StaticKeystore>) -> Result<Box<dyn Reply>, Rejection> {
let jwks = key_store.export_jwks().await;
Ok(Box::new(warp::reply::json(&jwks)))
}

View File

@ -12,8 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::sync::Arc;
use hyper::Method; use hyper::Method;
use mas_config::{CookiesConfig, OAuth2Config}; use mas_config::{CookiesConfig, OAuth2Config};
use mas_jose::StaticKeystore;
use mas_templates::Templates; use mas_templates::Templates;
use mas_warp_utils::filters::cors::cors; use mas_warp_utils::filters::cors::cors;
use sqlx::PgPool; use sqlx::PgPool;
@ -36,15 +39,16 @@ use self::{
pub fn filter( pub fn filter(
pool: &PgPool, pool: &PgPool,
templates: &Templates, templates: &Templates,
key_store: &Arc<StaticKeystore>,
oauth2_config: &OAuth2Config, oauth2_config: &OAuth2Config,
cookies_config: &CookiesConfig, cookies_config: &CookiesConfig,
) -> BoxedFilter<(impl Reply,)> { ) -> BoxedFilter<(impl Reply,)> {
let discovery = discovery(oauth2_config); let discovery = discovery(oauth2_config);
let keys = keys(oauth2_config); let keys = keys(key_store);
let authorization = authorization(pool, templates, oauth2_config, cookies_config); let authorization = authorization(pool, templates, oauth2_config, cookies_config);
let userinfo = userinfo(pool, oauth2_config); let userinfo = userinfo(pool, oauth2_config);
let introspection = introspection(pool, oauth2_config); let introspection = introspection(pool, oauth2_config);
let token = token(pool, oauth2_config); let token = token(pool, key_store, oauth2_config);
let filter = discovery let filter = discovery
.or(keys) .or(keys)

View File

@ -12,14 +12,16 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
use std::sync::Arc;
use anyhow::Context; use anyhow::Context;
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use data_encoding::BASE64URL_NOPAD; use data_encoding::BASE64URL_NOPAD;
use headers::{CacheControl, Pragma}; use headers::{CacheControl, Pragma};
use hyper::StatusCode; use hyper::StatusCode;
use jwt_compact::{Claims, Header, TimeOptions}; use mas_config::{OAuth2ClientConfig, OAuth2Config};
use mas_config::{KeySet, OAuth2ClientConfig, OAuth2Config};
use mas_data_model::{AuthorizationGrantStage, TokenType}; use mas_data_model::{AuthorizationGrantStage, TokenType};
use mas_jose::{DecodedJsonWebToken, JsonWebSignatureAlgorithm, SigningKeystore, StaticKeystore};
use mas_storage::{ use mas_storage::{
oauth2::{ oauth2::{
access_token::{add_access_token, revoke_access_token}, access_token::{add_access_token, revoke_access_token},
@ -30,7 +32,7 @@ use mas_storage::{
}; };
use mas_warp_utils::{ use mas_warp_utils::{
errors::WrapError, errors::WrapError,
filters::{client::client_authentication, database::connection, with_keys}, filters::{client::client_authentication, database::connection},
reply::with_typed_header, reply::with_typed_header,
}; };
use oauth2_types::{ use oauth2_types::{
@ -89,7 +91,12 @@ where
Err(Error { json, status }.into()) Err(Error { json, status }.into())
} }
pub fn filter(pool: &PgPool, oauth2_config: &OAuth2Config) -> BoxedFilter<(Box<dyn Reply>,)> { pub fn filter(
pool: &PgPool,
key_store: &Arc<StaticKeystore>,
oauth2_config: &OAuth2Config,
) -> BoxedFilter<(Box<dyn Reply>,)> {
let key_store = key_store.clone();
let audience = oauth2_config let audience = oauth2_config
.issuer .issuer
.join("/oauth2/token") .join("/oauth2/token")
@ -101,7 +108,7 @@ pub fn filter(pool: &PgPool, oauth2_config: &OAuth2Config) -> BoxedFilter<(Box<d
.and( .and(
warp::post() warp::post()
.and(client_authentication(oauth2_config, audience)) .and(client_authentication(oauth2_config, audience))
.and(with_keys(oauth2_config)) .and(warp::any().map(move || key_store.clone()))
.and(warp::any().map(move || issuer.clone())) .and(warp::any().map(move || issuer.clone()))
.and(connection(pool)) .and(connection(pool))
.and_then(token) .and_then(token)
@ -123,13 +130,14 @@ async fn token(
_auth: ClientAuthenticationMethod, _auth: ClientAuthenticationMethod,
client: OAuth2ClientConfig, client: OAuth2ClientConfig,
req: AccessTokenRequest, req: AccessTokenRequest,
keys: KeySet, key_store: Arc<StaticKeystore>,
issuer: Url, issuer: Url,
mut conn: PoolConnection<Postgres>, mut conn: PoolConnection<Postgres>,
) -> Result<Box<dyn Reply>, Rejection> { ) -> Result<Box<dyn Reply>, Rejection> {
let reply = match req { let reply = match req {
AccessTokenRequest::AuthorizationCode(grant) => { AccessTokenRequest::AuthorizationCode(grant) => {
let reply = authorization_code_grant(&grant, &client, &keys, issuer, &mut conn).await?; let reply =
authorization_code_grant(&grant, &client, &key_store, issuer, &mut conn).await?;
json(&reply) json(&reply)
} }
AccessTokenRequest::RefreshToken(grant) => { AccessTokenRequest::RefreshToken(grant) => {
@ -160,7 +168,7 @@ fn hash<H: Digest>(mut hasher: H, token: &str) -> anyhow::Result<String> {
async fn authorization_code_grant( async fn authorization_code_grant(
grant: &AuthorizationCodeGrant, grant: &AuthorizationCodeGrant,
client: &OAuth2ClientConfig, client: &OAuth2ClientConfig,
keys: &KeySet, key_store: &StaticKeystore,
issuer: Url, issuer: Url,
conn: &mut PoolConnection<Postgres>, conn: &mut PoolConnection<Postgres>,
) -> Result<AccessTokenResponse, Rejection> { ) -> Result<AccessTokenResponse, Rejection> {
@ -245,9 +253,8 @@ async fn authorization_code_grant(
.wrap_error()?; .wrap_error()?;
let id_token = if session.scope.contains(&OPENID) { let id_token = if session.scope.contains(&OPENID) {
let header = Header::default(); // TODO: time-related claims
let options = TimeOptions::default(); let claims = CustomClaims {
let claims = Claims::new(CustomClaims {
issuer, issuer,
subject: browser_session.user.sub.clone(), subject: browser_session.user.sub.clone(),
audiences: vec![client.client_id.clone()], audiences: vec![client.client_id.clone()],
@ -258,15 +265,15 @@ async fn authorization_code_grant(
.map(|a| a.created_at), .map(|a| a.created_at),
at_hash: hash(Sha256::new(), &access_token_str).wrap_error()?, at_hash: hash(Sha256::new(), &access_token_str).wrap_error()?,
c_hash: hash(Sha256::new(), &grant.code).wrap_error()?, c_hash: hash(Sha256::new(), &grant.code).wrap_error()?,
}) };
.set_duration_and_issuance(&options, Duration::minutes(30)); let header = key_store
let id_token = keys .prepare_header(JsonWebSignatureAlgorithm::Rs256)
.token(mas_config::Algorithm::Rs256, header, claims)
.await .await
.context("could not sign ID token")
.wrap_error()?; .wrap_error()?;
let id_token = DecodedJsonWebToken::new(header, claims);
let id_token = id_token.sign(key_store).await.wrap_error()?;
Some(id_token) Some(id_token.serialize())
} else { } else {
None None
}; };

31
crates/jose/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "mas-jose"
version = "0.1.0"
authors = ["Quentin Gliech <quenting@element.io>"]
edition = "2021"
license = "Apache-2.0"
[dependencies]
anyhow = "1.0.51"
async-trait = "0.1.52"
base64ct = { version = "1.0.1", features = ["std"] }
crypto-mac = { version = "0.11.1", features = ["std"] }
digest = "0.9.0"
ecdsa = { version = "0.13.3", features = ["sign", "verify", "pem", "pkcs8"] }
elliptic-curve = { version = "0.11.6", features = ["ecdh", "pem"] }
hmac = "0.11.0"
p256 = { version = "0.10.0", features = ["ecdsa", "pem", "pkcs8"] }
pkcs1 = { version = "0.3.1", features = ["pem", "pkcs8"] }
pkcs8 = { version = "0.8.0", features = ["pem"] }
rand = "0.8.4"
rsa = { git = "https://github.com/sandhose/RSA.git", branch = "bump-pkcs" }
sec1 = "0.2.1"
serde = { version = "1.0.132", features = ["derive"] }
serde_json = "1.0.73"
serde_with = { version = "1.11.0", features = ["base64"] }
sha2 = "0.9.8"
signature = { version = "1.4.0" }
thiserror = "1.0.30"
tokio = { version = "1.15.0", features = ["macros", "rt"] }
url = { version = "2.2.2", features = ["serde"] }
zeroize = "1.4.3"

295
crates/jose/src/iana.rs Normal file
View File

@ -0,0 +1,295 @@
// 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.
//! Generated enums from the IANA JOSE registry
//!
//! <https://www.iana.org/assignments/jose/jose.xhtml>
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonWebSignatureAlgorithm {
/// HMAC using SHA-256
#[serde(rename = "HS256")]
Hs256,
/// HMAC using SHA-384
#[serde(rename = "HS384")]
Hs384,
/// HMAC using SHA-512
#[serde(rename = "HS512")]
Hs512,
/// RSASSA-PKCS1-v1_5 using SHA-256
#[serde(rename = "RS256")]
Rs256,
/// RSASSA-PKCS1-v1_5 using SHA-384
#[serde(rename = "RS384")]
Rs384,
/// RSASSA-PKCS1-v1_5 using SHA-512
#[serde(rename = "RS512")]
Rs512,
/// ECDSA using P-256 and SHA-256
#[serde(rename = "ES256")]
Es256,
/// ECDSA using P-384 and SHA-384
#[serde(rename = "ES384")]
Es384,
/// ECDSA using P-521 and SHA-512
#[serde(rename = "ES512")]
Es512,
/// RSASSA-PSS using SHA-256 and MGF1 with SHA-256
#[serde(rename = "PS256")]
Ps256,
/// RSASSA-PSS using SHA-384 and MGF1 with SHA-384
#[serde(rename = "PS384")]
Ps384,
/// RSASSA-PSS using SHA-512 and MGF1 with SHA-512
#[serde(rename = "PS512")]
Ps512,
/// No digital signature or MAC performed
#[serde(rename = "none")]
None,
/// RSAES-PKCS1-v1_5
#[serde(rename = "RSA1_5")]
Rsa15,
/// RSAES OAEP using default parameters
#[serde(rename = "RSA-OAEP")]
RsaOaep,
/// RSAES OAEP using SHA-256 and MGF1 with SHA-256
#[serde(rename = "RSA-OAEP-256")]
RsaOaep256,
/// AES Key Wrap using 128-bit key
#[serde(rename = "A128KW")]
A128Kw,
/// AES Key Wrap using 192-bit key
#[serde(rename = "A192KW")]
A192Kw,
/// AES Key Wrap using 256-bit key
#[serde(rename = "A256KW")]
A256Kw,
/// Direct use of a shared symmetric key
#[serde(rename = "dir")]
Dir,
/// ECDH-ES using Concat KDF
#[serde(rename = "ECDH-ES")]
EcdhEs,
/// ECDH-ES using Concat KDF and "A128KW" wrapping
#[serde(rename = "ECDH-ES+A128KW")]
EcdhEsA128Kw,
/// ECDH-ES using Concat KDF and "A192KW" wrapping
#[serde(rename = "ECDH-ES+A192KW")]
EcdhEsA192Kw,
/// ECDH-ES using Concat KDF and "A256KW" wrapping
#[serde(rename = "ECDH-ES+A256KW")]
EcdhEsA256Kw,
/// Key wrapping with AES GCM using 128-bit key
#[serde(rename = "A128GCMKW")]
A128Gcmkw,
/// Key wrapping with AES GCM using 192-bit key
#[serde(rename = "A192GCMKW")]
A192Gcmkw,
/// Key wrapping with AES GCM using 256-bit key
#[serde(rename = "A256GCMKW")]
A256Gcmkw,
/// PBES2 with HMAC SHA-256 and "A128KW" wrapping
#[serde(rename = "PBES2-HS256+A128KW")]
Pbes2Hs256A128Kw,
/// PBES2 with HMAC SHA-384 and "A192KW" wrapping
#[serde(rename = "PBES2-HS384+A192KW")]
Pbes2Hs384A192Kw,
/// PBES2 with HMAC SHA-512 and "A256KW" wrapping
#[serde(rename = "PBES2-HS512+A256KW")]
Pbes2Hs512A256Kw,
/// EdDSA signature algorithms
#[serde(rename = "EdDSA")]
EdDsa,
/// RSA-OAEP using SHA-384 and MGF1 with SHA-384
#[serde(rename = "RSA-OAEP-384")]
RsaOaep384,
/// RSA-OAEP using SHA-512 and MGF1 with SHA-512
#[serde(rename = "RSA-OAEP-512")]
RsaOaep512,
/// ECDSA using secp256k1 curve and SHA-256
#[serde(rename = "ES256K")]
Es256K,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonWebEncryptionAlgorithm {
/// AES_128_CBC_HMAC_SHA_256 authenticated encryption algorithm
#[serde(rename = "A128CBC-HS256")]
A128CbcHs256,
/// AES_192_CBC_HMAC_SHA_384 authenticated encryption algorithm
#[serde(rename = "A192CBC-HS384")]
A192CbcHs384,
/// AES_256_CBC_HMAC_SHA_512 authenticated encryption algorithm
#[serde(rename = "A256CBC-HS512")]
A256CbcHs512,
/// AES GCM using 128-bit key
#[serde(rename = "A128GCM")]
A128Gcm,
/// AES GCM using 192-bit key
#[serde(rename = "A192GCM")]
A192Gcm,
/// AES GCM using 256-bit key
#[serde(rename = "A256GCM")]
A256Gcm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonWebEncryptionCompressionAlgorithm {
/// DEFLATE
#[serde(rename = "DEF")]
Def,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonWebKeyType {
/// Elliptic Curve
#[serde(rename = "EC")]
Ec,
/// RSA
#[serde(rename = "RSA")]
Rsa,
/// Octet sequence
#[serde(rename = "oct")]
Oct,
/// Octet string key pairs
#[serde(rename = "OKP")]
Okp,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonWebKeyEcEllipticCurve {
/// P-256 Curve
#[serde(rename = "P-256")]
P256,
/// P-384 Curve
#[serde(rename = "P-384")]
P384,
/// P-521 Curve
#[serde(rename = "P-521")]
P521,
/// SECG secp256k1 curve
#[serde(rename = "secp256k1")]
Secp256K1,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonWebKeyOkpEllipticCurve {
/// Ed25519 signature algorithm key pairs
#[serde(rename = "Ed25519")]
Ed25519,
/// Ed448 signature algorithm key pairs
#[serde(rename = "Ed448")]
Ed448,
/// X25519 function key pairs
#[serde(rename = "X25519")]
X25519,
/// X448 function key pairs
#[serde(rename = "X448")]
X448,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonWebKeyUse {
/// Digital Signature or MAC
#[serde(rename = "sig")]
Sig,
/// Encryption
#[serde(rename = "enc")]
Enc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum JsonWebKeyOperation {
/// Compute digital signature or MAC
#[serde(rename = "sign")]
Sign,
/// Verify digital signature or MAC
#[serde(rename = "verify")]
Verify,
/// Encrypt content
#[serde(rename = "encrypt")]
Encrypt,
/// Decrypt content and validate decryption, if applicable
#[serde(rename = "decrypt")]
Decrypt,
/// Encrypt key
#[serde(rename = "wrapKey")]
WrapKey,
/// Decrypt key and validate decryption, if applicable
#[serde(rename = "unwrapKey")]
UnwrapKey,
/// Derive key
#[serde(rename = "deriveKey")]
DeriveKey,
/// Derive bits not to be used as a key
#[serde(rename = "deriveBits")]
DeriveBits,
}

364
crates/jose/src/jwk.rs Normal file
View File

@ -0,0 +1,364 @@
// 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.
//! Ref: <https://www.rfc-editor.org/rfc/rfc7517.html>
use anyhow::bail;
use p256::NistP256;
use rsa::{BigUint, PublicKeyParts};
use serde::{Deserialize, Serialize};
use serde_with::{
base64::{Base64, Standard, UrlSafe},
formats::{Padded, Unpadded},
serde_as, skip_serializing_none,
};
use url::Url;
use crate::iana::{
JsonWebKeyEcEllipticCurve, JsonWebKeyOkpEllipticCurve, JsonWebKeyOperation, JsonWebKeyUse,
JsonWebSignatureAlgorithm,
};
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonWebKey {
#[serde(flatten)]
parameters: JsonWebKeyParameters,
#[serde(default)]
r#use: Option<JsonWebKeyUse>,
#[serde(default)]
key_ops: Option<Vec<JsonWebKeyOperation>>,
#[serde(default)]
alg: Option<JsonWebSignatureAlgorithm>,
#[serde(default)]
kid: Option<String>,
#[serde(default)]
x5u: Option<Url>,
#[serde(default)]
#[serde_as(as = "Option<Vec<Base64<Standard, Padded>>>")]
x5c: Option<Vec<Vec<u8>>>,
#[serde(default)]
#[serde_as(as = "Option<Base64<UrlSafe, Unpadded>>")]
x5t: Option<Vec<u8>>,
#[serde(default, rename = "x5t#S256")]
#[serde_as(as = "Option<Base64<UrlSafe, Unpadded>>")]
x5t_s256: Option<Vec<u8>>,
}
impl JsonWebKey {
#[must_use]
pub fn new(parameters: JsonWebKeyParameters) -> Self {
Self {
parameters,
r#use: None,
key_ops: None,
alg: None,
kid: None,
x5u: None,
x5c: None,
x5t: None,
x5t_s256: None,
}
}
#[must_use]
pub fn with_use(mut self, value: JsonWebKeyUse) -> Self {
self.r#use = Some(value);
self
}
#[must_use]
pub fn with_key_ops(mut self, key_ops: Vec<JsonWebKeyOperation>) -> Self {
self.key_ops = Some(key_ops);
self
}
#[must_use]
pub fn with_alg(mut self, alg: JsonWebSignatureAlgorithm) -> Self {
self.alg = Some(alg);
self
}
#[must_use]
pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
self.kid = Some(kid.into());
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JsonWebKeySet {
keys: Vec<JsonWebKey>,
}
impl JsonWebKeySet {
#[must_use]
pub fn new(keys: Vec<JsonWebKey>) -> Self {
Self { keys }
}
}
#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kty")]
pub enum JsonWebKeyParameters {
#[serde(rename = "RSA")]
Rsa {
#[serde_as(as = "Base64<UrlSafe, Unpadded>")]
n: Vec<u8>,
#[serde_as(as = "Base64<UrlSafe, Unpadded>")]
e: Vec<u8>,
},
#[serde(rename = "EC")]
Ec {
crv: JsonWebKeyEcEllipticCurve,
#[serde_as(as = "Base64<UrlSafe, Unpadded>")]
x: Vec<u8>,
#[serde_as(as = "Base64<UrlSafe, Unpadded>")]
y: Vec<u8>,
},
#[serde(rename = "OKP")]
Okp {
crv: JsonWebKeyOkpEllipticCurve,
#[serde_as(as = "Base64<UrlSafe, Unpadded>")]
x: Vec<u8>,
},
}
impl TryFrom<JsonWebKeyParameters> for ecdsa::VerifyingKey<NistP256> {
type Error = anyhow::Error;
fn try_from(params: JsonWebKeyParameters) -> Result<Self, Self::Error> {
let (x, y): ([u8; 32], [u8; 32]) = match params {
JsonWebKeyParameters::Ec {
x,
y,
crv: JsonWebKeyEcEllipticCurve::P256,
} => (
x.try_into()
.map_err(|_| anyhow::anyhow!("invalid curve parameter x"))?,
y.try_into()
.map_err(|_| anyhow::anyhow!("invalid curve parameter y"))?,
),
_ => bail!("Wrong key type"),
};
let point = sec1::EncodedPoint::from_affine_coordinates(&x.into(), &y.into(), false);
let key = ecdsa::VerifyingKey::from_encoded_point(&point)?;
Ok(key)
}
}
impl From<ecdsa::VerifyingKey<NistP256>> for JsonWebKeyParameters {
fn from(key: ecdsa::VerifyingKey<NistP256>) -> Self {
let points = key.to_encoded_point(false);
JsonWebKeyParameters::Ec {
x: points.x().unwrap().to_vec(),
y: points.y().unwrap().to_vec(),
crv: JsonWebKeyEcEllipticCurve::P256,
}
}
}
impl TryFrom<JsonWebKeyParameters> for rsa::RsaPublicKey {
type Error = anyhow::Error;
fn try_from(params: JsonWebKeyParameters) -> Result<Self, Self::Error> {
let (n, e) = match &params {
JsonWebKeyParameters::Rsa { n, e } => (n, e),
_ => bail!("Wrong key type"),
};
let n = BigUint::from_bytes_be(n);
let e = BigUint::from_bytes_be(e);
Ok(rsa::RsaPublicKey::new(n, e)?)
}
}
impl From<rsa::RsaPublicKey> for JsonWebKeyParameters {
fn from(key: rsa::RsaPublicKey) -> Self {
JsonWebKeyParameters::Rsa {
n: key.n().to_bytes_be(),
e: key.e().to_bytes_be(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_google_keys() {
let jwks = r#"{
"keys": [
{
"alg": "RS256",
"kty": "RSA",
"n": "tCwhHOxX_ylh5kVwfVqW7QIBTIsPjkjCjVCppDrynuF_3msEdtEaG64eJUz84ODFNMCC0BQ57G7wrKQVWkdSDxWUEqGk2BixBiHJRWZdofz1WOBTdPVicvHW5Zl_aIt7uXWMdOp_SODw-O2y2f05EqbFWFnR2-1y9K8KbiOp82CD72ny1Jbb_3PxTs2Z0F4ECAtTzpDteaJtjeeueRjr7040JAjQ-5fpL5D1g8x14LJyVIo-FL_y94NPFbMp7UCi69CIfVHXFO8WYFz949og-47mWRrID5lS4zpx-QLuvNhUb_lSqmylUdQB3HpRdOcYdj3xwy4MHJuu7tTaf0AmCQ",
"use": "sig",
"kid": "d98f49bc6ca4581eae8dfadd494fce10ea23aab0",
"e": "AQAB"
},
{
"use": "sig",
"kty": "RSA",
"kid": "03e84aed4ef4431014e8617567864c4efaaaede9",
"n": "ma2uRyBeSEOatGuDpCiV9oIxlDWix_KypDYuhQfEzqi_BiF4fV266OWfyjcABbam59aJMNvOnKW3u_eZM-PhMCBij5MZ-vcBJ4GfxDJeKSn-GP_dJ09rpDcILh8HaWAnPmMoi4DC0nrfE241wPISvZaaZnGHkOrfN_EnA5DligLgVUbrA5rJhQ1aSEQO_gf1raEOW3DZ_ACU3qhtgO0ZBG3a5h7BPiRs2sXqb2UCmBBgwyvYLDebnpE7AotF6_xBIlR-Cykdap3GHVMXhrIpvU195HF30ZoBU4dMd-AeG6HgRt4Cqy1moGoDgMQfbmQ48Hlunv9_Vi2e2CLvYECcBw",
"e": "AQAB",
"alg": "RS256"
}
]
}"#;
let jwks: JsonWebKeySet = serde_json::from_str(jwks).unwrap();
// Both keys are RSA public keys
for jwk in jwks.keys {
rsa::RsaPublicKey::try_from(jwk.parameters).unwrap();
}
}
#[allow(clippy::too_many_lines)]
#[test]
fn load_keycloak_keys() {
let jwks = r#"{
"keys": [
{
"kid": "SuGUPE9Sr-1Gha2NLse33r5NQu3XoS_I3Qds3bcmfQE",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "j21ih2m1RPeTXtIPFas2ZclhW8v2RitLdXJTqOFviWonaSObUWNZUkVvIdDKDyJhU7caGPnz52zXX1Trhbbq1uoCalAuIPw9UgJUJhUhlH7lqaRtYdbOrOzXZ7kVsApe1OdlezgShnyMhW5ChEJXQrCkR_LktBJQ8-6ZBNLHx3ps-pQrpXky_XdYZM_I_f1R8z36gnXagklAMMNKciFRURBMAsPbOgaly-slEDdVcuNtcoccSYdo9kRS5wjQlK6LZ3lniJrLRkUMvN6ZQcMLUWMDpghH5bdbhaaOb28HQWwpRDEBIMIH9Fi9aiKxwHa5YAqW1yetOq_9XXyYiuP9G6hZozSnkkfAOzYFqfr92vIPHddVVUUVLvH8UL4u1o553uVtOExA_pJVRghfO0IPZhJ6rUaZR7krvUMdCYngGznuD_V2-TAL9Nu8YXHIrZSU4WBKIvQC2HDOogSjj5dNDBUuAmOhI2OjuLjiOXpRPlaGcMIIlLALwQ76gFTEhTDlRXar7oLU8wj1KHLkc6d__lwdBkR-2Fr4dAewW4bHVFsPeDSM_vJZpK0XACrNgrrNBax48_hOlK9YfzSopyVCHwewxmC743eNYWEhE9LY-cc3ZGK9tHXgQG2l1tOZ_JK9wo1HsIuu3gdl2SV3ZOs6Ggi812GMfrgijnthC7e4Mv8",
"e": "AQAB",
"x5c": [
"MIIElTCCAn0CBgF95wE6HzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNDE3WhcNMzExMjIzMTExNTU3WjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCPbWKHabVE95Ne0g8VqzZlyWFby/ZGK0t1clOo4W+JaidpI5tRY1lSRW8h0MoPImFTtxoY+fPnbNdfVOuFturW6gJqUC4g/D1SAlQmFSGUfuWppG1h1s6s7NdnuRWwCl7U52V7OBKGfIyFbkKEQldCsKRH8uS0ElDz7pkE0sfHemz6lCuleTL9d1hkz8j9/VHzPfqCddqCSUAww0pyIVFREEwCw9s6BqXL6yUQN1Vy421yhxxJh2j2RFLnCNCUrotneWeImstGRQy83plBwwtRYwOmCEflt1uFpo5vbwdBbClEMQEgwgf0WL1qIrHAdrlgCpbXJ606r/1dfJiK4/0bqFmjNKeSR8A7NgWp+v3a8g8d11VVRRUu8fxQvi7Wjnne5W04TED+klVGCF87Qg9mEnqtRplHuSu9Qx0JieAbOe4P9Xb5MAv027xhccitlJThYEoi9ALYcM6iBKOPl00MFS4CY6EjY6O4uOI5elE+VoZwwgiUsAvBDvqAVMSFMOVFdqvugtTzCPUocuRzp3/+XB0GRH7YWvh0B7BbhsdUWw94NIz+8lmkrRcAKs2Cus0FrHjz+E6Ur1h/NKinJUIfB7DGYLvjd41hYSET0tj5xzdkYr20deBAbaXW05n8kr3CjUewi67eB2XZJXdk6zoaCLzXYYx+uCKOe2ELt7gy/wIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQB+mzE9ZA/hX/GAM74ZXs+ZEjV+qzUsGNpHkXyzdRc1ic28Go5ujAIMxwwsJ4PUSmw6MjPpCKV3kSXoyc7kUDZ/NQ7gwanP4DN8wDq7GLGqT3QRzMLfVy+el2Vjwd3Q6BhXNAK/jPzv0DFu1GG4WCpc1PcM8p/zWkbKWf9u4nBl7RBsMddn7KLxV+D2Y2eGshZ81YVaJiKF9y+gpgyxBOOsTFITu8SxBpXSwBIP4jTv7NllicxI8G9mk87XX3DdA+NHPKsKj35RbDAXyMid8tMl4R3IQ34F3ADuquHpdAdfTNDSm5lwilyWjV35O+8mKA2n/3LAhfCNgxMU0m9Jm8kI/pu9qTXnIx+HMr8IsAMseGxl+dZ/jJjGGPw1VZhHhU78dN+DZlUSKOVjOSQF+8CGuCxMnOx7+leGafs6G6LtsF/vQvJBTB9DRlM3ag0hQRT2ZEXPWSvcz3ARXqWyaHTzhR4F/+rRX1CyBsCdG3b3iicjGp7EPeaqXEki1K3SNwwv1byeJfqP785auswpojpUYfp/J850VAfA4xuVvxK3xuJrvbpS4DR6JQPY0fs6g8JEDahYa6rSB8H9toLC2r92gerqcGFpEU8uHRHxm9QZjIyFh78LWqpfegz0HMjYqaULgZJxqqZH2sVIu+nPuKC7tIjYWtODR0A13Ar3lH8aZg=="
],
"x5t": "fvgfH2gggONL7t4ZTvOdBpI94kM",
"x5t#S256": "uwHwO2crQ74jak2bmAeAt_4nrqGDQoElaiVvOlSGOOw"
},
{
"kid": "7pW7bkOM27LQ-KJGHzT1dt3yBmhcj20xj7A-itsuY6U",
"kty": "RSA",
"alg": "RS384",
"use": "sig",
"n": "lI1actdwWsMY8BpY68x8No7fwokLTTcZ8-qpqF9CDwX40X70ql9JPqTpLAHp7H7byfO-8VqZVKYKdzFCLjaEqs6Vx6YYuu4BsM2RIDI2CmClngUE5RMXnaEj8XP-h8Q4FnGcXL47n2UNr9mbZSp85W0TWOLtMczuqwwJ2jcYkDFtvLY0UirioKzN5Vr29WdDiCm9i4jHvHE7W41LFCOFLOLxGOq9wLVRNRMRcC3YS6WlrfiMFkPQIGxzFH2OiW2iR9x8QHmxqrqdfidmFsosgG5_2tbX3Q5PnHjYTNHh-iY4uIQ6bsBj1Enoj5h5kudwtgHDyn9OAiljTqLMXsoK9KEZrjE8zPnxQtvfXLCby2CI69X5JZ2lQJCch4cn1eIxn-jJ9Z0aE9EML1Bfp6w5sKELXt1aRtu5HQ5IQ__y2sBJd91NdiBxAzCK5kZjhRIRtt57J5ZHTLsBeHvr2L7SwZ_FojrQly7mI5PMGthZoGoVAr-bJcInzICpcsLKWdW-C6jxhXwRtnJOuTizEOr33vnLohMlmJUZiomYnKv8MEFAmihK5GAHTJ-4QIUuUeC13Dl5aRJacxvoKfgR_zw9P6HCUb7Nq7uzN3oqUdmDYYng1OFVo-1liYuCLbH6ep5LTmAstQY3IjkIFKeY-tvSPdpC9y1TwaHqEktXckvRGx0",
"e": "AQAB",
"x5c": [
"MIIElTCCAn0CBgF95wGjLjANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNDQzWhcNMzExMjIzMTExNjIzWjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCUjVpy13BawxjwGljrzHw2jt/CiQtNNxnz6qmoX0IPBfjRfvSqX0k+pOksAensftvJ877xWplUpgp3MUIuNoSqzpXHphi67gGwzZEgMjYKYKWeBQTlExedoSPxc/6HxDgWcZxcvjufZQ2v2ZtlKnzlbRNY4u0xzO6rDAnaNxiQMW28tjRSKuKgrM3lWvb1Z0OIKb2LiMe8cTtbjUsUI4Us4vEY6r3AtVE1ExFwLdhLpaWt+IwWQ9AgbHMUfY6JbaJH3HxAebGqup1+J2YWyiyAbn/a1tfdDk+ceNhM0eH6Jji4hDpuwGPUSeiPmHmS53C2AcPKf04CKWNOosxeygr0oRmuMTzM+fFC299csJvLYIjr1fklnaVAkJyHhyfV4jGf6Mn1nRoT0QwvUF+nrDmwoQte3VpG27kdDkhD//LawEl33U12IHEDMIrmRmOFEhG23nsnlkdMuwF4e+vYvtLBn8WiOtCXLuYjk8wa2FmgahUCv5slwifMgKlywspZ1b4LqPGFfBG2ck65OLMQ6vfe+cuiEyWYlRmKiZicq/wwQUCaKErkYAdMn7hAhS5R4LXcOXlpElpzG+gp+BH/PD0/ocJRvs2ru7M3eipR2YNhieDU4VWj7WWJi4Itsfp6nktOYCy1BjciOQgUp5j629I92kL3LVPBoeoSS1dyS9EbHQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCBKgIXOSH8cLgKHq1Q5Zn69YdVpC8W8gp3hfjqa9lpER8MHyZVw0isOzdICrNZdgsatq/uaYBMkc3LwxDRWJVN8AmKabqy6UDlAwHf7IUJJMcu3ODG+2tsy3U1SGIVWIffpOfv3F/gXxU76IXWnHiUzjCMYnWJg0Oy0G2oCDHk/7h82Dmq688UmPW+ycZhktjZ8lXqlopVZhjssTa48xJjtDdwN8OVmPGpV/uVzlDTCuYbyVWTYrEfnKwwVhzmAoIYc4XxDKZQ/z1zqE3HtIGrems7lGpgry55JMIRSYxoD2gg2YscDvuCnfzITwTPjijuyI7ocP6eA13FHriIcfHYEzKENUoEgWeybgs09JyIp3yE7YelL94vY4xJRVeL1jMmP5Wi6pM9cMKgQwkUzq7tmupkh9c6jF+tPStByDvD11ybJi5A/S2Rmer2qhlgnsml4NHkMZgIcWtokxoGmXoMcz6AOx31nRvvBHjC2emVnUmzojTCc5mPY3TRgzlAb+cQE/JIreZMfhfLwk4ny5dq+r4ya02fo7BrDA8oJJAP0gC82KNW5aZVpZSbkeRdogTVWdmiNYxvq95gI4ijLneYwSgWb1PM+CRhlNY7neJEv0VT5fbMd0XQZnxzSzQVymPiBHMEJBUul6UuxjVlJb7cdCtIty0zEWO3/uaEzqQl3w=="
],
"x5t": "Fk9zR2uLwBS6fHJbxM08TjDhUi8",
"x5t#S256": "ZiBGLQCaqehbgYF5A2dicp7WaL-zE4UTbFYyHKXDU_o"
},
{
"kid": "Jnf5fTyMpeiUyJnc3PHJaM9pR6VjWejv9RVyJgPugFs",
"kty": "RSA",
"alg": "RS512",
"use": "sig",
"n": "m3Y_aeHLL00X-bBPF3ySQ5ebOQ0dz40IQ4uWwWzL59zxn1AwzqrfrfAkKt_RJvJycfmy4zFeu89bNI86r6PtQVSvLqRYKo9UI4Y5jXs4HyvGvSL-DOXl8b8ybpo-o3bEiTgGOvIw2NGv49xT-_3SJ4Rba6awqVxkj334eZunrfvwYG9bjbAgPqWgMcuLVQNdNpytRHMB8Cjnd0SouL1dVxHlgHpYsZcRbsTsvPO1fRHcQRel44CgQRCZ08BvgETrF_9eATiRKBz18XbhaCZfSqh3a7IA-w9e236w6oD4ATOigeMHYZ0sfqKeoCsSd4rQ9kVc-U_EtL73_BVV7pmM4Xcl8JB8vzi_FMQVotzj5SgawylIxRdWUOGjyVFcUJ_u-DikoneVway0T4fXFJkWUflIoqf5-lHmMupb32q0E_pNL728yOlBfqm3bfJF9SF9w-h2SFMHWdRUzVOrtDRdrJVReGPPWvUHByALLL6B33FEcHDIcw4wqSfEmD6ypYJQxX8Er3_X9QFCgkn_rYUitUx90jOZ0n5vhubYnhiXX3RpeOCh9gF2O3h9Tv-DrynUO6OOgUSsBBbI-tGC5ebT51P0IJRkK3i4TkIYZnv7lj2auGWMC0-o7w24k_fG4U0EAr9N2cenR3Pepl6pjTa2g3y3C5_0LDUrcd67QPKl6ZE",
"e": "AQAB",
"x5c": [
"MIIElTCCAn0CBgF95wHdoDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNDU4WhcNMzExMjIzMTExNjM4WjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCbdj9p4csvTRf5sE8XfJJDl5s5DR3PjQhDi5bBbMvn3PGfUDDOqt+t8CQq39Em8nJx+bLjMV67z1s0jzqvo+1BVK8upFgqj1QjhjmNezgfK8a9Iv4M5eXxvzJumj6jdsSJOAY68jDY0a/j3FP7/dInhFtrprCpXGSPffh5m6et+/Bgb1uNsCA+paAxy4tVA102nK1EcwHwKOd3RKi4vV1XEeWAelixlxFuxOy887V9EdxBF6XjgKBBEJnTwG+AROsX/14BOJEoHPXxduFoJl9KqHdrsgD7D17bfrDqgPgBM6KB4wdhnSx+op6gKxJ3itD2RVz5T8S0vvf8FVXumYzhdyXwkHy/OL8UxBWi3OPlKBrDKUjFF1ZQ4aPJUVxQn+74OKSid5XBrLRPh9cUmRZR+Uiip/n6UeYy6lvfarQT+k0vvbzI6UF+qbdt8kX1IX3D6HZIUwdZ1FTNU6u0NF2slVF4Y89a9QcHIAssvoHfcURwcMhzDjCpJ8SYPrKlglDFfwSvf9f1AUKCSf+thSK1TH3SM5nSfm+G5tieGJdfdGl44KH2AXY7eH1O/4OvKdQ7o46BRKwEFsj60YLl5tPnU/QglGQreLhOQhhme/uWPZq4ZYwLT6jvDbiT98bhTQQCv03Zx6dHc96mXqmNNraDfLcLn/QsNStx3rtA8qXpkQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQAf2H6GjobSvc50L+cXeizzG6rg6Sm3x31PB7AH7XlVI+cytWA0X04IhuX+9H2VdEqujSApY/WM9voneyEm1eC3L6p4StO7icB+H4GYctzY+KV0qlbH3iMQkz+xngOTaEj+c9lSZlG7FSlL7Eybjjlj9mLyNJv4aiW7lQCxTWu7RcFq+w2ogvR7iv4uwbY9SHO/Fs5qbwzNIP65W9abcZvEAZKXQ69jOZ01VhNqiIA2D0OstjLWTfGaO0WxrUxvBVRqB3a86qIIwHjatrqdoGasLLGz8bAU3rY2b/DwZ7VBljUuZ+7PlysSK3w22k6eQe5G+XgxSl4Mzn+6lzCdoXeSVUzvQZrk+JBaDTVN5V5fteHSjLcaGNwIg9qYOHdx7PBYhbHP/hXADSQH90xIMipG168NOGBaxw+ybCaD6Eg+PfsPGnXO0Wnnd0PN/Dz4LggTLBwlbWaIDltj++0Xxlf375MrK1A9mDkhcdAOzZtkBkTD9UeXqL6UD0R0CFHp0B+TQEZuOuKRMKmlA2eo8f8z70vGToYk5TW/lvi8Li44+Y7UGLlLirpOtfBI35TPLK0OGfLh1dfqnuFQACObk+Ia+ON//r203sSQYQf3Qcq7u5KC/S406W+dSJ+c7Cf+8piMVc42PhYemdrkEPgzuTmzTJga2HFQk8BCUwoL1euMdw=="
],
"x5t": "bPku6_PBAoke1DpEcT0ghZYp6Fc",
"x5t#S256": "kIo7Hxj-A4jrwOBfo87c2kmAZzs87OHSd8tS4s_PGgk"
},
{
"kid": "WerdZfF_9ZgxLyHepk92CsKAEubvCs3rIAAy6wrUZUc",
"kty": "RSA",
"alg": "PS256",
"use": "sig",
"n": "85fgcXq_tB48BI8oeF9gjeWqL1opGtHoXv4rmwaxwfwzFU2ywJWRIEjwcJ_ypMPdC1im_kz_VCqWZBFyXfpuaEFkcsIAlLLnklI2TPUD3SV5taV_TXA61fm59K59iJDJr9EaQ_j5WJRGRluJpAi_q55U1vBWAHtnweL9RveQ-Ykc_qhpCcGDIek3-tAvJtVCpKQb764tkvmBD3pUPYTdVKHW4TAp4wFcgcj78E-xWELfm0T1nr7kZu-mV9DGYBZhFIWkf0lm4KA6NVDwWe-d1k-20FpT1tNsugK2Zx7SX2N5ytM2bCLH88Fcphvh9Bw_t7GgtZ9PvihJXdJcHR8nqlCsRMsGpeS6tnEl4E8StcTccgOkw1n2FJ-xxLM9eMOcfY--B9eKSaLRjLrhvWfa5-MGpB5JFrB4Rv17SD02Uoz1lwogCXPzTbKkBJhiA-YDinTRyGzyHTNXWsrmOLXrVRXUqdNYG32mpy1m3cSpoz9fOWne2dKKj9eawxFHa-GCzdfX3JBfgVKGGgaL5E_HlkJxx9OHNfQQQ4_OjyzqQGCoPG7jDCn9svb7hOE2epmYywShCgCsL_DZmTm3OdVWMLZ6oi77SIytWSx8QDy5KNCx3YsSLDg7sWv6t58gerWv1gkjhFzhyi3mqsw53WkeUyInrLoDYzEPkjWv3kSKQeM",
"e": "AQAB",
"x5c": [
"MIIElTCCAn0CBgF95wIdDjANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNTE1WhcNMzExMjIzMTExNjU1WjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDzl+Bxer+0HjwEjyh4X2CN5aovWika0ehe/iubBrHB/DMVTbLAlZEgSPBwn/Kkw90LWKb+TP9UKpZkEXJd+m5oQWRywgCUsueSUjZM9QPdJXm1pX9NcDrV+bn0rn2IkMmv0RpD+PlYlEZGW4mkCL+rnlTW8FYAe2fB4v1G95D5iRz+qGkJwYMh6Tf60C8m1UKkpBvvri2S+YEPelQ9hN1UodbhMCnjAVyByPvwT7FYQt+bRPWevuRm76ZX0MZgFmEUhaR/SWbgoDo1UPBZ753WT7bQWlPW02y6ArZnHtJfY3nK0zZsIsfzwVymG+H0HD+3saC1n0++KEld0lwdHyeqUKxEywal5Lq2cSXgTxK1xNxyA6TDWfYUn7HEsz14w5x9j74H14pJotGMuuG9Z9rn4wakHkkWsHhG/XtIPTZSjPWXCiAJc/NNsqQEmGID5gOKdNHIbPIdM1dayuY4tetVFdSp01gbfaanLWbdxKmjP185ad7Z0oqP15rDEUdr4YLN19fckF+BUoYaBovkT8eWQnHH04c19BBDj86PLOpAYKg8buMMKf2y9vuE4TZ6mZjLBKEKAKwv8NmZObc51VYwtnqiLvtIjK1ZLHxAPLko0LHdixIsODuxa/q3nyB6ta/WCSOEXOHKLeaqzDndaR5TIiesugNjMQ+SNa/eRIpB4wIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQDtW7hL3dWY0Nu87SkAPweBLocyI/S2/XZogBByzqdEWZru+26xQoUacqgYbrmQ6frQfWwlfpuzp7HBheDAHVobjlhl2jUQ7xO5vzTiB1bd/X1cQgOdTHosqiyTXLRBJKr3GQyfjrS3ruWKScGg5Y4jYGbsAoO3cNProddFeLbak0aQXGkhyWib2CzqtIpBA9Zy7EJYIWd5O+tExNIv+mjhSZZ6s3qdWXo/4RkVzBeGx5PApdoI/B7y0vwg4Dlt8qB9JcV9WL4nzI4s8foPMXuXgg+HJllB+NkSnTlQj77oU3pbrBoYgVhEdbfYkQuIdwYOWBQi/hdmV0YjUQQTAjYKBFKWQWCoAVKnfMpbDkdjN8KhOzohZ7KEahvHsnFt/PnS5MlFseZN9e6k4MB96EQ4fem7n/sPx4zqvhZMrPCaUT616hfCTa3DPoHzi2CxebE7GE95veQOtk9jCsXEbqKPvZ83/dfz5ftWu5wGHnhIK9S5sCCgjo2RA8bCLBl6/tBpmE0BwWqQqSZEs4zyXTplko822aJyxJtYprmDK0Ktxm6IEjSpEDCLuirnpQ0+Z8w19Key58Kx+OhNHczJK9wEaygKBQC1vvPV8ZvcHOx4XJgL8QwbPhaR5706YRfXTBceK2aw+oWzoNLJ4X7B2LB9IA84pJZKW+VfmnBz52iiqw=="
],
"x5t": "Xdy9viGu6isFknWeThJbh2_r4Qo",
"x5t#S256": "-toFY0ysJ3uopRPDNIQBo2VV_XT5YkniW2I-6XK_2oc"
},
{
"kid": "JjGFU4NwBkjaNRmIEw5BpoggXtG8dsl4s7gs29eYvno",
"kty": "RSA",
"alg": "PS384",
"use": "sig",
"n": "h3RNtfVqZPTQuFYBN54gOgcLX7bK-3qUyXstFso_V09RCHLHbFZV_czEC30lRQ6U5QeZ7iFpu7GbiM1csBk4HqhQ2v0TnjlQxIv9-71VV1JPZHrKsDFZlSr4HlZhkt6myBH16aDBT56U8pKg4oAVkoYS4dpzsR0q30zzrKAMgHRLTYWbCaGGGa1BuEUF9WgUhVnuiMu4ay9Tv0auu1UsdTkXjdR2YcWv2AihvFb4xYUSMBQr0bvUeMF_AAJ0B0VrGWIb51nARO2PNimKviHnFTrlaOyFJsnzwiiijuaOx2HZMQfcObzTz4Hx_YYIexOS83bYYkyGgvgUdu0wqls7ChgaZ_qiQdNnr_RWahIN2iVhjyOJuqsFsXufvHYo0nB1BFm1gnDHgYXdJIrSPql4g9gh1NZD_P0PuniPq3jvPoiQJ2u_9a8RDe9Scb_KzRgrBk0tkaXELDw1Q7ccJx9HUUbTxNkzNtZ6Z4MiKT4n0Bx4joglnL1BXvM5yrlO89brXAmfZgx6OmH7Dractz_Bny6QUHwF5vLMhMuVXsC5dU6UbkSZq82S5SnwnLHAe4JBOC-FTB08wAKgQXat16MIqmrBuKVtdNSshAxMk0wd_jPe-G4A_2RJ6pXSrQOkUFNPrOfV_PQMqI92zCYbIByWEwdfQAkavR2HC0-iDd202NM",
"e": "AQAB",
"x5c": [
"MIIElTCCAn0CBgF95wJajzANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNTMwWhcNMzExMjIzMTExNzEwWjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCHdE219Wpk9NC4VgE3niA6Bwtftsr7epTJey0Wyj9XT1EIcsdsVlX9zMQLfSVFDpTlB5nuIWm7sZuIzVywGTgeqFDa/ROeOVDEi/37vVVXUk9kesqwMVmVKvgeVmGS3qbIEfXpoMFPnpTykqDigBWShhLh2nOxHSrfTPOsoAyAdEtNhZsJoYYZrUG4RQX1aBSFWe6Iy7hrL1O/Rq67VSx1OReN1HZhxa/YCKG8VvjFhRIwFCvRu9R4wX8AAnQHRWsZYhvnWcBE7Y82KYq+IecVOuVo7IUmyfPCKKKO5o7HYdkxB9w5vNPPgfH9hgh7E5LzdthiTIaC+BR27TCqWzsKGBpn+qJB02ev9FZqEg3aJWGPI4m6qwWxe5+8dijScHUEWbWCcMeBhd0kitI+qXiD2CHU1kP8/Q+6eI+reO8+iJAna7/1rxEN71Jxv8rNGCsGTS2RpcQsPDVDtxwnH0dRRtPE2TM21npngyIpPifQHHiOiCWcvUFe8znKuU7z1utcCZ9mDHo6YfsOtpy3P8GfLpBQfAXm8syEy5VewLl1TpRuRJmrzZLlKfCcscB7gkE4L4VMHTzAAqBBdq3XowiqasG4pW101KyEDEyTTB3+M974bgD/ZEnqldKtA6RQU0+s59X89Ayoj3bMJhsgHJYTB19ACRq9HYcLT6IN3bTY0wIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQAoudkN4cTAnT2b7cd/JklLFLBnw+mwSgj0ZYyRByBiC0AXU+LmM+D1Bs0TRqXKICBZ2dxKRr8Z1PdQe8BghWcl84iLXEjHVdw08/xVaQ5GKcGLOfSRG+3Suj6UyZfwcMJtX4GO919fX10mAlk6ySHe6SViSVMup5ePwA0C7Jws9/aXNLIvw82hIX8IVM1kuuu3DICQlr1nsvbu6XVQT5kdhIpApr+IDrBvNFWKPdH+vA8Kxb8wkhk9HIUbAi3WqftHoiI8Qq92BYcB5gjwocAkzmrDDoAulEM24+IJoK87DWEeC0Vu9kOB7i5PKXqUANJ7ebQJJhgXy+xNq1Alh4f95mqolXCxdo1jJi/OFExLDr93Fk5QVRQxi2aSEDkoz/h7stzuUvvTyT75pJAILSL+xv8Gd/bYhL5lfCXcHA9uPDQwM/9gnA1ojIdF1bvgaEo2r1xoY/LAScTB0nzvRh1EVoZYxBHid+79MJWQq0vpJ58pyKcxgKaoD1pUQ2brAlYFNflNiMN18VnCF7vnY8Ol9Po881ee2TWLex0i5cLREo4fvPNg0QgoaQvDqlvJqr1nJll/Mzv2w9s3agQxPwKRkTOTb4jNOV23Uy0SbxBD42EOllLmUN897ra3pdmacHHMatw75Sfcu4WhuMrN13RzVUARMjFN+nNI8i7ay9WJOA=="
],
"x5t": "4ovci1k_HPeLoL2PhUrmoDlLQhU",
"x5t#S256": "PJsKbXoQ7tZoR7aRDli60V65BPtO-Q7QSpk5P5hDcLY"
},
{
"kid": "zesnP0SwjgVGBU5RPhqccF0W4BbMkbwtZpjAeTAgwz8",
"kty": "RSA",
"alg": "PS512",
"use": "sig",
"n": "x4NHNpmzOgqWgQGsiWTpyhdIkSSiO0hMKr_5oNNecp254CSO_zEPS6wWKMNwwZRteKIPzPafCkXvmGEuQo716CL9OP5T8BR25sXkws0llygfbbSK2dTWVN4lhM1Rm6zFJ4aK0BZo6EXDp0E0Od8SQSN7FooRAWOiO7HvjgpIdRyqkANElBSL7aNdsPP7dgVMua5P6MNfVjKCe93C-iqsOVadUV5UM3oblf6M_KkDV9GNr6oAizfrXHpPnHjG29u-DSsmCbLimgZaJ3LDnLrmzxbbl9b4mHJQqe00rNDUF6Q6BmmDgJGDMdPH4J8i6w_1z4Xll8Ul-UGHS6rJZeTVsEdKGSOoIbhQa9iuGxC_I_YIjkVbV3O8LcYBzDKetzups4R5CVFpwvAK03UCdM7yLkbDglWcSOYtbPVBafumCzyjWX9u7CpBAcVWe9KpEMVCYgi90TSkX2Vw1bPP07mTBFmK0fwmU2ZlDR0S9Q2NT9St7zWP6teuOeue7PAFlPUVotFdoh8ltZVLEfUTo81E1tiNycDCy9QTP9CzwplqpPIkmTdjmMCO6lollLrTm9SuXGp2FSUdE43tYEzRGNqsGpcwskkvzQWtl7bETaS5vCwPH76k6qGf-TpOHnOH1G7vDzDkewqJ-oscqwkdw4ONo_KxT-CGwv-JwMoSXWEtMKE",
"e": "AQAB",
"x5c": [
"MIIElTCCAn0CBgF95wKLoDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANkZXYwHhcNMjExMjIzMTExNTQzWhcNMzExMjIzMTExNzIzWjAOMQwwCgYDVQQDDANkZXYwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDHg0c2mbM6CpaBAayJZOnKF0iRJKI7SEwqv/mg015ynbngJI7/MQ9LrBYow3DBlG14og/M9p8KRe+YYS5CjvXoIv04/lPwFHbmxeTCzSWXKB9ttIrZ1NZU3iWEzVGbrMUnhorQFmjoRcOnQTQ53xJBI3sWihEBY6I7se+OCkh1HKqQA0SUFIvto12w8/t2BUy5rk/ow19WMoJ73cL6Kqw5Vp1RXlQzehuV/oz8qQNX0Y2vqgCLN+tcek+ceMbb274NKyYJsuKaBloncsOcuubPFtuX1viYclCp7TSs0NQXpDoGaYOAkYMx08fgnyLrD/XPheWXxSX5QYdLqsll5NWwR0oZI6ghuFBr2K4bEL8j9giORVtXc7wtxgHMMp63O6mzhHkJUWnC8ArTdQJ0zvIuRsOCVZxI5i1s9UFp+6YLPKNZf27sKkEBxVZ70qkQxUJiCL3RNKRfZXDVs8/TuZMEWYrR/CZTZmUNHRL1DY1P1K3vNY/q1645657s8AWU9RWi0V2iHyW1lUsR9ROjzUTW2I3JwMLL1BM/0LPCmWqk8iSZN2OYwI7qWiWUutOb1K5canYVJR0Tje1gTNEY2qwalzCySS/NBa2XtsRNpLm8LA8fvqTqoZ/5Ok4ec4fUbu8PMOR7Con6ixyrCR3Dg42j8rFP4IbC/4nAyhJdYS0woQIDAQABMA0GCSqGSIb3DQEBCwUAA4ICAQCr+GGCVS/sBHukLZay8WlBXtowJ6qyX8hMFClDGDN9/c3mUbLJsCCVN6Jbr33BgNZ/ZuvLhUvhWGPlOXUB3Rf+qRzNEzoLVwanw2yCUEKFi6AvuBUY9twNnifH4y1Cg34NVaZoPvQ0hlOLGYl9CCxen7VMLJ5QbTC8H3fPX1prWOic5x46Bu7IqoEqZtDszt8F+uteruRsHVHCiWx5dW7goeIa8YsUK0A4mnOy5kViSvs5L6Kq0N5uCB9EDu/Ew5R0/mi/UTm5L8CpzQig1pmvDtIy7ZnosHu7zYGSQiR04jn3Od0rdWzTCcs8W79+ewgJ0bdYmfvSnVehs1BR+cjivzBqMWMqdyz6eQXCy/esiG5KDIxH4F0HGLiiwXqHUYjJPex8TId+fz0MFScrEN5fjE+XltGzsPwlcgnAqE0pN0ExJSHwzBHNkJJQpjHrsEurWn9QGBqD75Vt9yVeHE8MZ4zMGj3ZkRmn1x6wVBdv1V12P3e4b8V5aG02FbREkJzFTXtGyDHtw/hlWGz9M9w0c5TAI6xYPa1gS6/Fw95J6S0V3n3JH+xqi6yv2H2cQHukFxFSPJW1cc/hh5DJ4Ag8+pKuO1Vdo9p+DltaGLWBabON7GZZojlYdx2WtBZK9CMRgrxobg+OBA44AHkiWkhflrqGLYul866wiNu6zLEfdQ=="
],
"x5t": "0lMdqEAhOWfUXDivtS-KwPvwKNY",
"x5t#S256": "aOjQ1awJmcaF7Yiz75ifjBKbjr4Eo-Ha5uNMi-TtuGw"
},
{
"kid": "VlsIs1LssBo6r8EuXJo81rDEoTYpUjiMkeq_PlapKfY",
"kty": "EC",
"alg": "ES256",
"use": "sig",
"crv": "P-256",
"x": "3kqy7us0mepJJblWwj0Exg2S7PtWaJvB7SI_ptg0jrA",
"y": "S5Z8d4AfCvRL-hUd6Pv-L3tH6H9T4RIwO2tvBS0hj1A"
},
{
"kid": "1yWLiqf8sa-em0hSbtZEjKmrardmQdYLR9gpzsypMCU",
"kty": "EC",
"alg": "ES384",
"use": "sig",
"crv": "P-384",
"x": "i4YYGQZd5QQ1JpUXcrZe5wpCid3pqFLnzxxy89Chn-NQ1oYDPTP2M8V9sfazeuB0",
"y": "xf4qN2ZuMLVh4GmRVt1PHhQooB2o61pF0lHrBlIod5hVamiRtUo_Np9PikPD8Uap"
},
{
"kid": "V5EwcLp9vmwAnstzI1Ndba-iWkX5oTBHK7GnYTyfuOE",
"kty": "EC",
"alg": "ES512",
"use": "sig",
"crv": "P-521",
"x": "rScgdd_n2cHLyzZvP8zw0u9vQyhu0VsbfQypheS7aDoHRLcXccPQTsmrQLrLuKX8PPkITjL_BJDSm7Bo8gv5Sd4",
"y": "Vu3rTFNn_9zWTki95UGT1Bd9PN84KDXmttCrJ1bsYHTWQCaEONk8iwA3U6mEDrg4xtZSTXXKCFdFP13ONWB9oZ4"
}
]
}"#;
let jwks: JsonWebKeySet = serde_json::from_str(jwks).unwrap();
// The first 6 keys are RSA, 7th is P-256
let mut keys = jwks.keys.into_iter();
rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap();
rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap();
rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap();
rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap();
rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap();
rsa::RsaPublicKey::try_from(keys.next().unwrap().parameters).unwrap();
ecdsa::VerifyingKey::try_from(keys.next().unwrap().parameters).unwrap();
}
}

267
crates/jose/src/jwt.rs Normal file
View File

@ -0,0 +1,267 @@
// 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::str::FromStr;
use base64ct::{Base64UrlUnpadded, Encoding};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_with::{
base64::{Base64, Standard, UrlSafe},
formats::{Padded, Unpadded},
serde_as, skip_serializing_none,
};
use url::Url;
use crate::{
iana::{
JsonWebEncryptionAlgorithm, JsonWebEncryptionCompressionAlgorithm,
JsonWebSignatureAlgorithm,
},
jwk::JsonWebKey,
SigningKeystore, VerifyingKeystore,
};
#[serde_as]
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize)]
pub struct JwtHeader {
alg: JsonWebSignatureAlgorithm,
#[serde(default)]
enc: Option<JsonWebEncryptionAlgorithm>,
#[serde(default)]
jku: Option<Url>,
#[serde(default)]
jwk: Option<JsonWebKey>,
#[serde(default)]
kid: Option<String>,
#[serde(default)]
x5u: Option<Url>,
#[serde(default)]
#[serde_as(as = "Option<Vec<Base64<Standard, Padded>>>")]
x5c: Option<Vec<Vec<u8>>>,
#[serde(default)]
#[serde_as(as = "Option<Base64<UrlSafe, Unpadded>>")]
x5t: Option<Vec<u8>>,
#[serde(default, rename = "x5t#S256")]
#[serde_as(as = "Option<Base64<UrlSafe, Unpadded>>")]
x5t_s256: Option<Vec<u8>>,
#[serde(default)]
typ: Option<String>,
#[serde(default)]
cty: Option<String>,
#[serde(default)]
crit: Option<Vec<String>>,
#[serde(default)]
zip: Option<JsonWebEncryptionCompressionAlgorithm>,
}
impl JwtHeader {
pub fn encode(&self) -> anyhow::Result<String> {
let payload = serde_json::to_string(self)?;
let encoded = Base64UrlUnpadded::encode_string(payload.as_bytes());
Ok(encoded)
}
#[must_use]
pub fn new(alg: JsonWebSignatureAlgorithm) -> Self {
Self {
alg,
enc: None,
jku: None,
jwk: None,
kid: None,
x5u: None,
x5c: None,
x5t: None,
x5t_s256: None,
typ: None,
cty: None,
crit: None,
zip: None,
}
}
#[must_use]
pub fn alg(&self) -> JsonWebSignatureAlgorithm {
self.alg
}
#[must_use]
pub fn kid(&self) -> Option<&str> {
self.kid.as_deref()
}
#[must_use]
pub fn with_kid(mut self, kid: impl Into<String>) -> Self {
self.kid = Some(kid.into());
self
}
}
impl FromStr for JwtHeader {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let decoded = Base64UrlUnpadded::decode_vec(s)?;
let parsed = serde_json::from_slice(&decoded)?;
Ok(parsed)
}
}
#[derive(Debug)]
pub struct JsonWebTokenParts {
payload: String,
signature: Vec<u8>,
}
impl FromStr for JsonWebTokenParts {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (payload, signature) = s
.rsplit_once('.')
.ok_or_else(|| anyhow::anyhow!("no dots found in JWT"))?;
let signature = Base64UrlUnpadded::decode_vec(signature)?;
let payload = payload.to_owned();
Ok(Self { payload, signature })
}
}
pub struct DecodedJsonWebToken<T> {
header: JwtHeader,
payload: T,
}
impl<T> DecodedJsonWebToken<T>
where
T: Serialize,
{
fn serialize(&self) -> anyhow::Result<String> {
let header = serde_json::to_vec(&self.header)?;
let header = Base64UrlUnpadded::encode_string(&header);
let payload = serde_json::to_vec(&self.payload)?;
let payload = Base64UrlUnpadded::encode_string(&payload);
Ok(format!("{}.{}", header, payload))
}
pub async fn sign<S: SigningKeystore>(&self, store: S) -> anyhow::Result<JsonWebTokenParts> {
let payload = self.serialize()?;
let signature = store.sign(&self.header, payload.as_bytes()).await?;
Ok(JsonWebTokenParts { payload, signature })
}
}
impl<T> DecodedJsonWebToken<T> {
pub fn new(header: JwtHeader, payload: T) -> Self {
Self { header, payload }
}
pub fn claims(&self) -> &T {
&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 async fn verify<T, S: VerifyingKeystore>(
&self,
decoded: &DecodedJsonWebToken<T>,
store: S,
) -> anyhow::Result<()> {
store
.verify(&decoded.header, self.payload.as_bytes(), &self.signature)
.await?;
Ok(())
}
pub async fn decode_and_verify<T: DeserializeOwned, S: VerifyingKeystore>(
&self,
store: S,
) -> anyhow::Result<DecodedJsonWebToken<T>> {
let decoded = self.decode()?;
self.verify(&decoded, 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)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::SharedSecret;
#[tokio::test]
async fn decode_hs256() {
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
let jwt: JsonWebTokenParts = jwt.parse().unwrap();
let secret = "your-256-bit-secret";
println!("{:?}", jwt);
let store = SharedSecret::new(&secret);
let jwt: DecodedJsonWebToken<serde_json::Value> =
jwt.decode_and_verify(&store).await.unwrap();
assert_eq!(jwt.header.typ, Some("JWT".to_string()));
assert_eq!(jwt.header.alg, JsonWebSignatureAlgorithm::Hs256);
assert_eq!(
jwt.payload,
serde_json::json!({
"sub": "1234567890",
"name": "John Doe",
"iat": 1_516_239_022
})
);
}
}

510
crates/jose/src/keystore.rs Normal file
View File

@ -0,0 +1,510 @@
// 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;
use anyhow::bail;
use async_trait::async_trait;
use base64ct::{Base64UrlUnpadded, Encoding};
use digest::Digest;
use ecdsa::VerifyingKey;
use hmac::{Hmac, Mac, NewMac};
use p256::{NistP256, PublicKey};
use pkcs1::EncodeRsaPublicKey;
use pkcs8::EncodePublicKey;
use rsa::{PublicKey as _, RsaPublicKey};
use sha2::{Sha256, Sha384, Sha512};
use signature::{Signature, Signer, Verifier};
use crate::{
iana::JsonWebSignatureAlgorithm, JsonWebKey, JsonWebKeyOperation, JsonWebKeySet, JwtHeader,
};
#[async_trait]
pub trait SigningKeystore {
async fn prepare_header(self, alg: JsonWebSignatureAlgorithm) -> anyhow::Result<JwtHeader>;
async fn sign(self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result<Vec<u8>>;
}
#[async_trait]
pub trait VerifyingKeystore {
async fn verify(self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> anyhow::Result<()>;
}
#[async_trait]
pub trait ExportJwks {
async fn export_jwks(self) -> JsonWebKeySet;
}
pub struct SharedSecret<'a> {
inner: &'a [u8],
}
impl<'a> SharedSecret<'a> {
pub fn new(source: &'a impl AsRef<[u8]>) -> Self {
Self {
inner: source.as_ref(),
}
}
}
#[async_trait]
impl<'a> SigningKeystore for &SharedSecret<'a> {
async fn prepare_header(self, alg: JsonWebSignatureAlgorithm) -> anyhow::Result<JwtHeader> {
if !matches!(
alg,
JsonWebSignatureAlgorithm::Hs256
| JsonWebSignatureAlgorithm::Hs384
| JsonWebSignatureAlgorithm::Hs512,
) {
bail!("unsupported algorithm")
}
Ok(JwtHeader::new(alg))
}
async fn sign(self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result<Vec<u8>> {
// TODO: do the signing in a blocking task
// TODO: should we bail out if the key is too small?
let signature = match header.alg() {
JsonWebSignatureAlgorithm::Hs256 => {
let mut mac = Hmac::<Sha256>::new_from_slice(self.inner)?;
mac.update(msg);
mac.finalize().into_bytes().to_vec()
}
JsonWebSignatureAlgorithm::Hs384 => {
let mut mac = Hmac::<Sha384>::new_from_slice(self.inner)?;
mac.update(msg);
mac.finalize().into_bytes().to_vec()
}
JsonWebSignatureAlgorithm::Hs512 => {
let mut mac = Hmac::<Sha512>::new_from_slice(self.inner)?;
mac.update(msg);
mac.finalize().into_bytes().to_vec()
}
_ => bail!("unsupported algorithm"),
};
Ok(signature)
}
}
#[async_trait]
impl<'a> VerifyingKeystore for &SharedSecret<'a> {
async fn verify(
self,
header: &JwtHeader,
payload: &[u8],
signature: &[u8],
) -> anyhow::Result<()> {
// TODO: do the verification in a blocking task
match header.alg() {
JsonWebSignatureAlgorithm::Hs256 => {
let mut mac = Hmac::<Sha256>::new_from_slice(self.inner)?;
mac.update(payload);
mac.verify(signature)?;
}
JsonWebSignatureAlgorithm::Hs384 => {
let mut mac = Hmac::<Sha384>::new_from_slice(self.inner)?;
mac.update(payload);
mac.verify(signature)?;
}
JsonWebSignatureAlgorithm::Hs512 => {
let mut mac = Hmac::<Sha512>::new_from_slice(self.inner)?;
mac.update(payload);
mac.verify(signature)?;
}
_ => bail!("unsupported algorithm"),
};
Ok(())
}
}
#[derive(Default)]
pub struct StaticKeystore {
rsa_keys: HashMap<String, rsa::RsaPrivateKey>,
es256_keys: HashMap<String, ecdsa::SigningKey<NistP256>>,
}
impl StaticKeystore {
#[must_use]
pub fn new() -> Self {
StaticKeystore::default()
}
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: ecdsa::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(())
}
}
#[async_trait]
impl SigningKeystore for &StaticKeystore {
async fn prepare_header(self, alg: JsonWebSignatureAlgorithm) -> anyhow::Result<JwtHeader> {
let header = JwtHeader::new(alg);
let kid = match alg {
JsonWebSignatureAlgorithm::Rs256
| JsonWebSignatureAlgorithm::Rs384
| JsonWebSignatureAlgorithm::Rs512 => self
.rsa_keys
.keys()
.next()
.ok_or_else(|| anyhow::anyhow!("no RSA keys in keystore"))?,
JsonWebSignatureAlgorithm::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: &JwtHeader, 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() {
JsonWebSignatureAlgorithm::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,
)?
}
JsonWebSignatureAlgorithm::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,
)?
}
JsonWebSignatureAlgorithm::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,
)?
}
JsonWebSignatureAlgorithm::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)
}
}
#[async_trait]
impl VerifyingKeystore for &StaticKeystore {
async fn verify(
self,
header: &JwtHeader,
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() {
JsonWebSignatureAlgorithm::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,
)?;
}
JsonWebSignatureAlgorithm::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,
)?;
}
JsonWebSignatureAlgorithm::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,
)?;
}
JsonWebSignatureAlgorithm::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 ExportJwks for &StaticKeystore {
async fn export_jwks(self) -> JsonWebKeySet {
let rsa = self.rsa_keys.iter().flat_map(|(kid, key)| {
let pubkey = RsaPublicKey::from(key);
let basekey = JsonWebKey::new(pubkey.into())
.with_kid(kid)
.with_use(crate::JsonWebKeyUse::Sig)
.with_key_ops(vec![JsonWebKeyOperation::Sign]);
let algs = [
JsonWebSignatureAlgorithm::Rs256,
JsonWebSignatureAlgorithm::Rs384,
JsonWebSignatureAlgorithm::Rs512,
];
algs.into_iter()
.map(move |alg| basekey.clone().with_alg(alg))
});
let es256 = self.es256_keys.iter().map(|(kid, key)| {
let pubkey = ecdsa::VerifyingKey::from(key);
JsonWebKey::new(pubkey.into())
.with_kid(kid)
.with_use(crate::JsonWebKeyUse::Sig)
.with_key_ops(vec![JsonWebKeyOperation::Sign])
.with_alg(JsonWebSignatureAlgorithm::Es256)
});
let keys = rsa.chain(es256).collect();
JsonWebKeySet::new(keys)
}
}
#[cfg(test)]
mod tests {
use ecdsa::SigningKey;
use pkcs1::DecodeRsaPrivateKey;
use pkcs8::DecodePrivateKey;
use rsa::RsaPrivateKey;
use super::*;
// Generate with
// openssl genrsa 2048
const 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 EC_PKCS8_PEM: &str = "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+tHxet7G+uar2Cef
iYPb7jv3uzncFtwJ7RhDOvEA0fChRANCAATCKn2AEqa9785k+TmwkeCvLub8XGrF
ezE6bA/blaPVE3nu4SUVYKULRJQxNjeOSra8TQrlIS8e5ItbMn8Tv9KV
-----END PRIVATE KEY-----";
#[tokio::test]
async fn test_shared_secret() {
let secret = "super-complicated-secret-that-should-be-big-enough-for-sha512";
let message = "this is the message to sign".as_bytes();
let store = SharedSecret::new(&secret);
for alg in [
JsonWebSignatureAlgorithm::Hs256,
JsonWebSignatureAlgorithm::Hs384,
JsonWebSignatureAlgorithm::Hs512,
] {
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();
}
}
#[tokio::test]
async fn test_static_store() {
let message = "this is the message to sign".as_bytes();
let store = {
let mut s = StaticKeystore::new();
let rsa = RsaPrivateKey::from_pkcs1_pem(RSA_PKCS1_PEM).unwrap();
s.add_rsa_key(rsa).unwrap();
let ecdsa = SigningKey::from_pkcs8_pem(EC_PKCS8_PEM).unwrap();
s.add_ecdsa_key(ecdsa).unwrap();
s
};
for alg in [
JsonWebSignatureAlgorithm::Rs256,
JsonWebSignatureAlgorithm::Rs384,
JsonWebSignatureAlgorithm::Rs512,
JsonWebSignatureAlgorithm::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();
}
}
}

35
crates/jose/src/lib.rs Normal file
View File

@ -0,0 +1,35 @@
// 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.
#![forbid(unsafe_code)]
#![deny(clippy::all)]
#![deny(rustdoc::broken_intra_doc_links)]
#![warn(clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::module_name_repetitions)]
pub(crate) mod iana;
pub(crate) mod jwk;
pub(crate) mod jwt;
mod keystore;
pub use self::{
iana::{
JsonWebEncryptionAlgorithm, JsonWebEncryptionCompressionAlgorithm, JsonWebKeyOperation,
JsonWebKeyType, JsonWebKeyUse, JsonWebSignatureAlgorithm,
},
jwk::{JsonWebKey, JsonWebKeySet},
jwt::{DecodedJsonWebToken, JsonWebTokenParts, JwtHeader},
keystore::{ExportJwks, SharedSecret, SigningKeystore, StaticKeystore, VerifyingKeystore},
};

View File

@ -159,6 +159,7 @@ impl TryInto<BrowserSession<PostgresqlBackend>> for SessionLookup {
} }
} }
#[tracing::instrument(skip_all, fields(session.id = id))]
pub async fn lookup_active_session( pub async fn lookup_active_session(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
id: i64, id: i64,
@ -191,6 +192,7 @@ pub async fn lookup_active_session(
Ok(res) Ok(res)
} }
#[tracing::instrument(skip_all, fields(user.id = user.data))]
pub async fn start_session( pub async fn start_session(
executor: impl PgExecutor<'_>, executor: impl PgExecutor<'_>,
user: User<PostgresqlBackend>, user: User<PostgresqlBackend>,

View File

@ -14,7 +14,6 @@ hyper = { version = "0.14.16", features = ["full"] }
thiserror = "1.0.30" thiserror = "1.0.30"
anyhow = "1.0.51" anyhow = "1.0.51"
sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres"] } sqlx = { version = "0.5.9", features = ["runtime-tokio-rustls", "postgres"] }
jwt-compact = { version = "0.5.0-beta.1", features = ["with_rsa", "k256"] }
chrono = { version = "0.4.19", features = ["serde"] } chrono = { version = "0.4.19", features = ["serde"] }
serde = { version = "1.0.131", features = ["derive"] } serde = { version = "1.0.131", features = ["derive"] }
serde_with = { version = "1.11.0", features = ["hex", "chrono"] } serde_with = { version = "1.11.0", features = ["hex", "chrono"] }
@ -35,3 +34,4 @@ mas-config = { path = "../config" }
mas-templates = { path = "../templates" } mas-templates = { path = "../templates" }
mas-data-model = { path = "../data-model" } mas-data-model = { path = "../data-model" }
mas-storage = { path = "../storage" } mas-storage = { path = "../storage" }
mas-jose = { path = "../jose" }

View File

@ -14,15 +14,9 @@
//! Handle client authentication //! Handle client authentication
use std::borrow::Cow;
use chrono::{Duration, Utc};
use headers::{authorization::Basic, Authorization}; use headers::{authorization::Basic, Authorization};
use jwt_compact::{
alg::{Hs256, Hs256Key, Hs384, Hs384Key, Hs512, Hs512Key},
Algorithm, AlgorithmExt, AlgorithmSignature, TimeOptions, Token, UntrustedToken,
};
use mas_config::{OAuth2ClientConfig, OAuth2Config}; use mas_config::{OAuth2ClientConfig, OAuth2Config};
use mas_jose::{DecodedJsonWebToken, JsonWebTokenParts, SharedSecret};
use oauth2_types::requests::ClientAuthenticationMethod; use oauth2_types::requests::ClientAuthenticationMethod;
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
@ -113,67 +107,6 @@ struct ClientAssertionClaims {
jwt_id: Option<String>, jwt_id: Option<String>,
} }
struct UnsignedSignature(Vec<u8>);
impl AlgorithmSignature for UnsignedSignature {
fn try_from_slice(slice: &[u8]) -> anyhow::Result<Self> {
Ok(Self(slice.to_vec()))
}
fn as_bytes(&self) -> std::borrow::Cow<'_, [u8]> {
Cow::Borrowed(&self.0)
}
}
struct Unsigned<'a>(&'a str);
impl<'a> Algorithm for Unsigned<'a> {
type SigningKey = ();
type VerifyingKey = ();
type Signature = UnsignedSignature;
fn name(&self) -> std::borrow::Cow<'static, str> {
Cow::Owned(self.0.to_string())
}
fn sign(&self, _signing_key: &Self::SigningKey, _message: &[u8]) -> Self::Signature {
UnsignedSignature(Vec::new())
}
fn verify_signature(
&self,
_signature: &Self::Signature,
_verifying_key: &Self::VerifyingKey,
_message: &[u8],
) -> bool {
true
}
}
fn verify_token(
untrusted_token: &UntrustedToken,
key: &str,
) -> anyhow::Result<Token<ClientAssertionClaims>> {
match untrusted_token.algorithm() {
"HS256" => {
let key = Hs256Key::new(key);
let token = Hs256.validate_integrity(untrusted_token, &key)?;
Ok(token)
}
"HS384" => {
let key = Hs384Key::new(key);
let token = Hs384.validate_integrity(untrusted_token, &key)?;
Ok(token)
}
"HS512" => {
let key = Hs512Key::new(key);
let token = Hs512.validate_integrity(untrusted_token, &key)?;
Ok(token)
}
alg => anyhow::bail!("unsupported signing algorithm {}", alg),
}
}
async fn authenticate_client<T>( async fn authenticate_client<T>(
clients: Vec<OAuth2ClientConfig>, clients: Vec<OAuth2ClientConfig>,
audience: String, audience: String,
@ -211,23 +144,13 @@ async fn authenticate_client<T>(
client_assertion_type: ClientAssertionType::JwtBearer, client_assertion_type: ClientAssertionType::JwtBearer,
client_assertion, client_assertion,
} => { } => {
let untrusted_token = UntrustedToken::new(&client_assertion).wrap_error()?; let token: JsonWebTokenParts = client_assertion.parse().wrap_error()?;
let decoded: DecodedJsonWebToken<ClientAssertionClaims> =
token.decode().wrap_error()?;
// client_id might have been passed as parameter. If not, it should be inferred // client_id might have been passed as parameter. If not, it should be inferred
// from the token, as per rfc7521 sec. 4.2 // from the token, as per rfc7521 sec. 4.2
// TODO: this is not a pretty way to do it let client_id = client_id.unwrap_or_else(|| decoded.claims().subject.clone());
let client_id = client_id
.ok_or(()) // Dumb error type
.or_else(|()| {
let alg = Unsigned(untrusted_token.algorithm());
// We need to deserialize the token once without verifying the signature to get
// the client_id
let token: Token<ClientAssertionClaims> =
alg.validate_integrity(&untrusted_token, &())?;
Ok::<_, anyhow::Error>(token.claims().custom.subject.clone())
})
.wrap_error()?;
let client = clients let client = clients
.iter() .iter()
@ -237,32 +160,20 @@ async fn authenticate_client<T>(
})?; })?;
if let Some(client_secret) = &client.client_secret { if let Some(client_secret) = &client.client_secret {
let token = verify_token(&untrusted_token, client_secret).wrap_error()?; let store = SharedSecret::new(client_secret);
token.verify(&decoded, &store).await.wrap_error()?;
let time_options = TimeOptions::new(Duration::minutes(1), Utc::now); let claims = decoded.claims();
// TODO: validate the times again
// rfc7523 sec. 3.4: expiration must be set and validated
let claims = token
.claims()
.validate_expiration(&time_options)
.wrap_error()?;
// rfc7523 sec. 3.5: "not before" can be set and must be validated if present
if claims.not_before.is_some() {
claims.validate_maturity(&time_options).wrap_error()?;
}
// rfc7523 sec. 3.3: the audience is the URL being called // rfc7523 sec. 3.3: the audience is the URL being called
if claims.custom.audience != audience { if claims.audience != audience {
Err(ClientAuthenticationError::AudienceMismatch { Err(ClientAuthenticationError::AudienceMismatch {
expected: audience, expected: audience,
got: claims.custom.audience.clone(), got: claims.audience.clone(),
}) })
// rfc7523 sec. 3.1 & 3.2: both the issuer and the subject must // rfc7523 sec. 3.1 & 3.2: both the issuer and the subject must
// match the client_id // match the client_id
} else if claims.custom.issuer != claims.custom.subject } else if claims.issuer != claims.subject || claims.issuer != client_id {
|| claims.custom.issuer != client_id
{
Err(ClientAuthenticationError::InvalidAssertion) Err(ClientAuthenticationError::InvalidAssertion)
} else { } else {
Ok(client) Ok(client)
@ -348,8 +259,8 @@ struct ClientAuthForm<T> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use headers::authorization::Credentials; use headers::authorization::Credentials;
use jwt_compact::{Claims, Header};
use mas_config::ConfigurationSection; use mas_config::ConfigurationSection;
use mas_jose::{JsonWebSignatureAlgorithm, SigningKeystore};
use serde_json::json; use serde_json::json;
use super::*; use super::*;
@ -385,38 +296,34 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn client_secret_jwt_hs256() { async fn client_secret_jwt_hs256() {
client_secret_jwt::<'_, Hs256>().await; client_secret_jwt(JsonWebSignatureAlgorithm::Hs256).await;
} }
#[tokio::test] #[tokio::test]
async fn client_secret_jwt_hs384() { async fn client_secret_jwt_hs384() {
client_secret_jwt::<'_, Hs384>().await; client_secret_jwt(JsonWebSignatureAlgorithm::Hs384).await;
} }
#[tokio::test] #[tokio::test]
async fn client_secret_jwt_hs512() { async fn client_secret_jwt_hs512() {
client_secret_jwt::<'_, Hs512>().await; client_secret_jwt(JsonWebSignatureAlgorithm::Hs512).await;
} }
async fn client_secret_jwt<'k, A>() async fn client_secret_jwt(alg: JsonWebSignatureAlgorithm) {
where
A: Algorithm + Default,
A::SigningKey: From<&'k [u8]>,
{
let audience = "https://example.com/token".to_string(); let audience = "https://example.com/token".to_string();
let filter = client_authentication::<Form>(&oauth2_config(), audience.clone()); let filter = client_authentication::<Form>(&oauth2_config(), audience.clone());
let time_options = TimeOptions::default();
let key = A::SigningKey::from(CLIENT_SECRET.as_bytes()); let store = SharedSecret::new(&CLIENT_SECRET);
let alg = A::default(); let claims = ClientAssertionClaims {
let header = Header::default();
let claims = Claims::new(ClientAssertionClaims {
issuer: "confidential".to_string(), issuer: "confidential".to_string(),
subject: "confidential".to_string(), subject: "confidential".to_string(),
audience, audience,
jwt_id: None, jwt_id: None,
}) };
.set_duration_and_issuance(&time_options, Duration::seconds(15)); let header = store.prepare_header(alg).await.expect("JWT header");
let jwt = DecodedJsonWebToken::new(header, claims);
let jwt = jwt.sign(&store).await.expect("signed token");
let jwt = jwt.serialize();
// TODO: test failing cases // TODO: test failing cases
// - expired token // - expired token
@ -425,16 +332,12 @@ mod tests {
// - audience mismatch // - audience mismatch
// - wrong secret/signature // - wrong secret/signature
let token = alg
.token(header, &claims, &key)
.expect("could not sign token");
let (auth, client, body) = warp::test::request() let (auth, client, body) = warp::test::request()
.method("POST") .method("POST")
.header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string()) .header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string())
.body(serde_urlencoded::to_string(json!({ .body(serde_urlencoded::to_string(json!({
"client_id": "confidential", "client_id": "confidential",
"client_assertion": token, "client_assertion": jwt,
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"foo": "baz", "foo": "baz",
"bar": "foobar", "bar": "foobar",
@ -453,7 +356,7 @@ mod tests {
.method("POST") .method("POST")
.header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string()) .header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string())
.body(serde_urlencoded::to_string(json!({ .body(serde_urlencoded::to_string(json!({
"client_assertion": token, "client_assertion": jwt,
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"foo": "baz", "foo": "baz",
"bar": "foobar", "bar": "foobar",
@ -467,7 +370,7 @@ mod tests {
.method("POST") .method("POST")
.body(serde_urlencoded::to_string(json!({ .body(serde_urlencoded::to_string(json!({
"client_id": "confidential-2", "client_id": "confidential-2",
"client_assertion": token, "client_assertion": jwt,
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer", "client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"foo": "baz", "foo": "baz",
"bar": "foobar", "bar": "foobar",

View File

@ -28,7 +28,6 @@ pub mod session;
use std::convert::Infallible; use std::convert::Infallible;
use mas_config::{KeySet, OAuth2Config};
use mas_templates::Templates; use mas_templates::Templates;
use warp::{Filter, Rejection}; use warp::{Filter, Rejection};
@ -43,15 +42,6 @@ pub fn with_templates(
warp::any().map(move || templates.clone()) warp::any().map(move || templates.clone())
} }
/// Extract the [`KeySet`] from the [`OAuth2Config`]
#[must_use]
pub fn with_keys(
oauth2_config: &OAuth2Config,
) -> impl Filter<Extract = (KeySet,), Error = Infallible> + Clone + Send + Sync + 'static {
let keyset = oauth2_config.keys.clone();
warp::any().map(move || keyset.clone())
}
/// Recover a particular rejection type with a `None` option variant /// Recover a particular rejection type with a `None` option variant
/// ///
/// # Example /// # Example