You've already forked authentication-service
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:
530
Cargo.lock
generated
530
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||||
|
|
||||||
|
@ -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" }
|
||||||
|
@ -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,
|
||||||
|
@ -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],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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,
|
||||||
|
@ -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)))
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
31
crates/jose/Cargo.toml
Normal 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
295
crates/jose/src/iana.rs
Normal 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
364
crates/jose/src/jwk.rs
Normal 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 ¶ms {
|
||||||
|
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
267
crates/jose/src/jwt.rs
Normal 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
510
crates/jose/src/keystore.rs
Normal 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
35
crates/jose/src/lib.rs
Normal 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},
|
||||||
|
};
|
@ -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>,
|
||||||
|
@ -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" }
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user