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
WIP: better JOSE
This commit is contained in:
@ -31,8 +31,8 @@ use mas_data_model::{Client, JwksOrJwksUri, StorageBackend};
|
||||
use mas_http::HttpServiceExt;
|
||||
use mas_iana::oauth::OAuthClientAuthenticationMethod;
|
||||
use mas_jose::{
|
||||
DecodedJsonWebToken, DynamicJwksStore, Either, JsonWebKeySet, JsonWebTokenParts, JwtHeader,
|
||||
SharedSecret, StaticJwksStore, VerifyingKeystore,
|
||||
DecodedJsonWebToken, DynamicJwksStore, Either, JsonWebKeySet, JsonWebSignatureHeader,
|
||||
JsonWebTokenParts, SharedSecret, StaticJwksStore, VerifyingKeystore,
|
||||
};
|
||||
use mas_storage::{
|
||||
oauth2::client::{lookup_client_by_client_id, ClientFetchError},
|
||||
@ -73,7 +73,7 @@ pub enum Credentials {
|
||||
ClientAssertionJwtBearer {
|
||||
client_id: String,
|
||||
jwt: JsonWebTokenParts,
|
||||
header: Box<JwtHeader>,
|
||||
header: Box<JsonWebSignatureHeader>,
|
||||
claims: HashMap<String, Value>,
|
||||
},
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ sqlx = { version = "0.6.1", features = ["runtime-tokio-rustls", "postgres"] }
|
||||
lettre = { version = "0.10.1", default-features = false, features = ["serde", "builder"] }
|
||||
|
||||
rand = "0.8.5"
|
||||
rsa = "0.7.0-pre"
|
||||
rsa = { git = "https://github.com/RustCrypto/RSA.git" }
|
||||
p256 = { version = "0.11.1", features = ["ecdsa", "pem", "pkcs8"] }
|
||||
pkcs8 = { version = "0.9.0", features = ["pem"] }
|
||||
chacha20poly1305 = { version = "0.10.1", features = ["std"] }
|
||||
|
@ -40,7 +40,7 @@ serde_urlencoded = "0.7.1"
|
||||
argon2 = { version = "0.4.1", features = ["password-hash"] }
|
||||
|
||||
# Crypto, hashing and signing stuff
|
||||
rsa = "0.7.0-pre"
|
||||
rsa = { git = "https://github.com/RustCrypto/RSA.git" }
|
||||
pkcs8 = { version = "0.9.0", features = ["pem"] }
|
||||
elliptic-curve = { version = "0.12.3", features = ["pem"] }
|
||||
sha2 = "0.10.2"
|
||||
|
@ -26,7 +26,7 @@ use mas_data_model::{AuthorizationGrantStage, Client, TokenType};
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
use mas_jose::{
|
||||
claims::{self, ClaimError},
|
||||
DecodedJsonWebToken, SigningKeystore, StaticKeystore,
|
||||
DecodedJsonWebToken, JwtSignatureError, SigningKeystore, StaticKeystore,
|
||||
};
|
||||
use mas_router::UrlBuilder;
|
||||
use mas_storage::{
|
||||
@ -173,6 +173,12 @@ impl From<ClaimError> for RouteError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<JwtSignatureError> for RouteError {
|
||||
fn from(e: JwtSignatureError) -> Self {
|
||||
Self::Internal(Box::new(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, err)]
|
||||
pub(crate) async fn post(
|
||||
client_authorization: ClientAuthorization<AccessTokenRequest>,
|
||||
|
@ -6,6 +6,7 @@ edition = "2021"
|
||||
license = "Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
async-signature = "0.2.0"
|
||||
anyhow = "1.0.62"
|
||||
async-trait = "0.1.57"
|
||||
base64ct = { version = "1.5.1", features = ["std"] }
|
||||
@ -21,7 +22,7 @@ p256 = { version = "0.11.1", features = ["ecdsa", "pem", "pkcs8"] }
|
||||
pkcs1 = { version = "0.4.0", features = ["pem", "pkcs8"] }
|
||||
pkcs8 = { version = "0.9.0", features = ["pem", "std"] }
|
||||
rand = "0.8.5"
|
||||
rsa = "0.7.0-pre"
|
||||
rsa = { git = "https://github.com/RustCrypto/RSA.git" }
|
||||
schemars = "0.8.10"
|
||||
sec1 = "0.3.0"
|
||||
serde = { version = "1.0.144", features = ["derive"] }
|
||||
|
143
crates/jose/src/jwt/header.rs
Normal file
143
crates/jose/src/jwt/header.rs
Normal file
@ -0,0 +1,143 @@
|
||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_with::{
|
||||
base64::{Base64, Standard, UrlSafe},
|
||||
formats::{Padded, Unpadded},
|
||||
serde_as, skip_serializing_none,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
use crate::jwk::JsonWebKey;
|
||||
|
||||
#[serde_as]
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct JsonWebSignatureHeader {
|
||||
alg: JsonWebSignatureAlg,
|
||||
|
||||
#[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>>,
|
||||
}
|
||||
|
||||
impl JsonWebSignatureHeader {
|
||||
#[must_use]
|
||||
pub fn new(alg: JsonWebSignatureAlg) -> Self {
|
||||
Self {
|
||||
alg,
|
||||
jku: None,
|
||||
jwk: None,
|
||||
kid: None,
|
||||
x5u: None,
|
||||
x5c: None,
|
||||
x5t: None,
|
||||
x5t_s256: None,
|
||||
typ: None,
|
||||
cty: None,
|
||||
crit: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn alg(&self) -> JsonWebSignatureAlg {
|
||||
self.alg
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn jku(&self) -> Option<&Url> {
|
||||
self.jku.as_ref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_jku(mut self, jku: Url) -> Self {
|
||||
self.jku = Some(jku);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn jwk(&self) -> Option<&JsonWebKey> {
|
||||
self.jwk.as_ref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_jwk(mut self, jwk: JsonWebKey) -> Self {
|
||||
self.jwk = Some(jwk);
|
||||
self
|
||||
}
|
||||
|
||||
#[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
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn typ(&self) -> Option<&str> {
|
||||
self.typ.as_deref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_typ(mut self, typ: String) -> Self {
|
||||
self.typ = Some(typ);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn crit(&self) -> Option<&[String]> {
|
||||
self.crit.as_deref()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_crit(mut self, crit: Vec<String>) -> Self {
|
||||
self.crit = Some(crit);
|
||||
self
|
||||
}
|
||||
}
|
@ -15,116 +15,16 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use base64ct::{Base64UrlUnpadded, Encoding};
|
||||
use mas_iana::jose::{
|
||||
JsonWebEncryptionCompressionAlgorithm, JsonWebEncryptionEnc, JsonWebSignatureAlg,
|
||||
};
|
||||
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 serde::{de::DeserializeOwned, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{jwk::JsonWebKey, SigningKeystore, VerifyingKeystore};
|
||||
use crate::{SigningKeystore, VerifyingKeystore};
|
||||
|
||||
#[serde_as]
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
|
||||
pub struct JwtHeader {
|
||||
alg: JsonWebSignatureAlg,
|
||||
mod header;
|
||||
mod raw;
|
||||
mod signed;
|
||||
|
||||
#[serde(default)]
|
||||
enc: Option<JsonWebEncryptionEnc>,
|
||||
|
||||
#[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: JsonWebSignatureAlg) -> 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) -> JsonWebSignatureAlg {
|
||||
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)
|
||||
}
|
||||
}
|
||||
pub use self::{header::JsonWebSignatureHeader, signed::Jwt};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct JsonWebTokenParts {
|
||||
@ -132,21 +32,62 @@ pub struct JsonWebTokenParts {
|
||||
signature: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("failed to decode JWT")]
|
||||
pub enum JwtPartsDecodeError {
|
||||
#[error("no dots found in the JWT")]
|
||||
NoDots,
|
||||
|
||||
#[error("could not decode signature")]
|
||||
SignatureEncoding {
|
||||
#[from]
|
||||
inner: base64ct::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl FromStr for JsonWebTokenParts {
|
||||
type Err = anyhow::Error;
|
||||
type Err = JwtPartsDecodeError;
|
||||
|
||||
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 (payload, signature) = s.rsplit_once('.').ok_or(JwtPartsDecodeError::NoDots)?;
|
||||
let signature = Base64UrlUnpadded::decode_vec(signature)?;
|
||||
let payload = payload.to_owned();
|
||||
Ok(Self { payload, signature })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("failed to serialize JWT")]
|
||||
pub enum JwtSerializeError {
|
||||
#[error("failed to serialize JWT header")]
|
||||
Header {
|
||||
#[source]
|
||||
inner: serde_json::Error,
|
||||
},
|
||||
|
||||
#[error("failed to serialize payload")]
|
||||
Payload {
|
||||
#[source]
|
||||
inner: serde_json::Error,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[error("failed to serialize JWT")]
|
||||
pub enum JwtSignatureError {
|
||||
Serialize {
|
||||
#[from]
|
||||
inner: JwtSerializeError,
|
||||
},
|
||||
|
||||
Sign {
|
||||
#[source]
|
||||
inner: anyhow::Error,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct DecodedJsonWebToken<T> {
|
||||
header: JwtHeader,
|
||||
header: JsonWebSignatureHeader,
|
||||
payload: T,
|
||||
}
|
||||
|
||||
@ -154,24 +95,33 @@ impl<T> DecodedJsonWebToken<T>
|
||||
where
|
||||
T: Serialize,
|
||||
{
|
||||
fn serialize(&self) -> anyhow::Result<String> {
|
||||
let header = serde_json::to_vec(&self.header)?;
|
||||
fn serialize(&self) -> Result<String, JwtSerializeError> {
|
||||
let header = serde_json::to_vec(&self.header)
|
||||
.map_err(|inner| JwtSerializeError::Header { inner })?;
|
||||
let header = Base64UrlUnpadded::encode_string(&header);
|
||||
let payload = serde_json::to_vec(&self.payload)?;
|
||||
|
||||
let payload = serde_json::to_vec(&self.payload)
|
||||
.map_err(|inner| JwtSerializeError::Payload { inner })?;
|
||||
let payload = Base64UrlUnpadded::encode_string(&payload);
|
||||
|
||||
Ok(format!("{}.{}", header, payload))
|
||||
}
|
||||
|
||||
pub async fn sign<S: SigningKeystore>(&self, store: &S) -> anyhow::Result<JsonWebTokenParts> {
|
||||
pub async fn sign<S: SigningKeystore>(
|
||||
&self,
|
||||
store: &S,
|
||||
) -> Result<JsonWebTokenParts, JwtSignatureError> {
|
||||
let payload = self.serialize()?;
|
||||
let signature = store.sign(&self.header, payload.as_bytes()).await?;
|
||||
let signature = store
|
||||
.sign(&self.header, payload.as_bytes())
|
||||
.await
|
||||
.map_err(|inner| JwtSignatureError::Sign { inner })?;
|
||||
Ok(JsonWebTokenParts { payload, signature })
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> DecodedJsonWebToken<T> {
|
||||
pub fn new(header: JwtHeader, payload: T) -> Self {
|
||||
pub fn new(header: JsonWebSignatureHeader, payload: T) -> Self {
|
||||
Self { header, payload }
|
||||
}
|
||||
|
||||
@ -179,11 +129,11 @@ impl<T> DecodedJsonWebToken<T> {
|
||||
&self.payload
|
||||
}
|
||||
|
||||
pub fn header(&self) -> &JwtHeader {
|
||||
pub fn header(&self) -> &JsonWebSignatureHeader {
|
||||
&self.header
|
||||
}
|
||||
|
||||
pub fn split(self) -> (JwtHeader, T) {
|
||||
pub fn split(self) -> (JsonWebSignatureHeader, T) {
|
||||
(self.header, self.payload)
|
||||
}
|
||||
}
|
||||
@ -213,7 +163,11 @@ impl JsonWebTokenParts {
|
||||
Ok(decoded)
|
||||
}
|
||||
|
||||
pub fn verify<S: VerifyingKeystore>(&self, header: &JwtHeader, store: &S) -> S::Future {
|
||||
pub fn verify<S: VerifyingKeystore>(
|
||||
&self,
|
||||
header: &JsonWebSignatureHeader,
|
||||
store: &S,
|
||||
) -> S::Future {
|
||||
store.verify(header, self.payload.as_bytes(), &self.signature)
|
||||
}
|
||||
|
||||
@ -239,6 +193,8 @@ impl JsonWebTokenParts {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
|
||||
use super::*;
|
||||
use crate::SharedSecret;
|
||||
|
||||
@ -247,13 +203,12 @@ mod tests {
|
||||
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_owned()));
|
||||
assert_eq!(jwt.header.alg, JsonWebSignatureAlg::Hs256);
|
||||
assert_eq!(jwt.header.typ(), Some("JWT"));
|
||||
assert_eq!(jwt.header.alg(), JsonWebSignatureAlg::Hs256);
|
||||
assert_eq!(
|
||||
jwt.payload,
|
||||
serde_json::json!({
|
99
crates/jose/src/jwt/raw.rs
Normal file
99
crates/jose/src/jwt/raw.rs
Normal file
@ -0,0 +1,99 @@
|
||||
// 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::{borrow::Cow, ops::Deref};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub struct RawJwt<'a> {
|
||||
inner: Cow<'a, str>,
|
||||
first_dot: usize,
|
||||
second_dot: usize,
|
||||
}
|
||||
|
||||
impl RawJwt<'static> {
|
||||
pub(super) fn new(inner: String, first_dot: usize, second_dot: usize) -> Self {
|
||||
Self {
|
||||
inner: inner.into(),
|
||||
first_dot,
|
||||
second_dot,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> std::fmt::Display for RawJwt<'a> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.inner)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RawJwt<'a> {
|
||||
pub fn header(&'a self) -> &'a str {
|
||||
&self.inner[..self.first_dot]
|
||||
}
|
||||
|
||||
pub fn payload(&'a self) -> &'a str {
|
||||
&self.inner[self.first_dot + 1..self.second_dot]
|
||||
}
|
||||
|
||||
pub fn signature(&'a self) -> &'a str {
|
||||
&self.inner[self.second_dot + 1..]
|
||||
}
|
||||
|
||||
pub fn signed_part(&'a self) -> &'a str {
|
||||
&self.inner[..self.second_dot]
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for RawJwt<'a> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DecodeError {
|
||||
#[error("no dots found in JWT")]
|
||||
NoDots,
|
||||
|
||||
#[error("only one dot found in JWT")]
|
||||
OnlyOneDot,
|
||||
|
||||
#[error("too many dots in JWT")]
|
||||
TooManyDots,
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for RawJwt<'a> {
|
||||
type Error = DecodeError;
|
||||
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
|
||||
let mut indices = value
|
||||
.char_indices()
|
||||
.filter_map(|(idx, c)| (c == '.').then(|| idx));
|
||||
|
||||
let first_dot = indices.next().ok_or(DecodeError::NoDots)?;
|
||||
let second_dot = indices.next().ok_or(DecodeError::OnlyOneDot)?;
|
||||
|
||||
if indices.next().is_some() {
|
||||
return Err(DecodeError::TooManyDots);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
inner: value.into(),
|
||||
first_dot,
|
||||
second_dot,
|
||||
})
|
||||
}
|
||||
}
|
278
crates/jose/src/jwt/signed.rs
Normal file
278
crates/jose/src/jwt/signed.rs
Normal file
@ -0,0 +1,278 @@
|
||||
// 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 base64ct::{Base64UrlUnpadded, Encoding};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use signature::{Signature, Signer, Verifier};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{header::JsonWebSignatureHeader, raw::RawJwt};
|
||||
|
||||
pub struct Jwt<'a, T> {
|
||||
raw: RawJwt<'a>,
|
||||
header: JsonWebSignatureHeader,
|
||||
payload: T,
|
||||
signature: Vec<u8>,
|
||||
}
|
||||
|
||||
impl<'a, T> std::fmt::Display for Jwt<'a, T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.raw)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> std::fmt::Debug for Jwt<'a, T>
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Jwt")
|
||||
.field("raw", &"...")
|
||||
.field("header", &self.header)
|
||||
.field("payload", &self.payload)
|
||||
.field("signature", &"...")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JwtDecodeError {
|
||||
#[error(transparent)]
|
||||
RawDecode {
|
||||
#[from]
|
||||
inner: super::raw::DecodeError,
|
||||
},
|
||||
|
||||
#[error("failed to decode JWT header")]
|
||||
DecodeHeader {
|
||||
#[source]
|
||||
inner: base64ct::Error,
|
||||
},
|
||||
|
||||
#[error("failed to deserialize JWT header")]
|
||||
DeserializeHeader {
|
||||
#[source]
|
||||
inner: serde_json::Error,
|
||||
},
|
||||
|
||||
#[error("failed to decode JWT payload")]
|
||||
DecodePayload {
|
||||
#[source]
|
||||
inner: base64ct::Error,
|
||||
},
|
||||
|
||||
#[error("failed to deserialize JWT payload")]
|
||||
DeserializePayload {
|
||||
#[source]
|
||||
inner: serde_json::Error,
|
||||
},
|
||||
|
||||
#[error("failed to decode JWT signature")]
|
||||
DecodeSignature {
|
||||
#[source]
|
||||
inner: base64ct::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl JwtDecodeError {
|
||||
fn decode_header(inner: base64ct::Error) -> Self {
|
||||
Self::DecodeHeader { inner }
|
||||
}
|
||||
|
||||
fn deserialize_header(inner: serde_json::Error) -> Self {
|
||||
Self::DeserializeHeader { inner }
|
||||
}
|
||||
|
||||
fn decode_payload(inner: base64ct::Error) -> Self {
|
||||
Self::DecodePayload { inner }
|
||||
}
|
||||
|
||||
fn deserialize_payload(inner: serde_json::Error) -> Self {
|
||||
Self::DeserializePayload { inner }
|
||||
}
|
||||
|
||||
fn decode_signature(inner: base64ct::Error) -> Self {
|
||||
Self::DecodeSignature { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> TryFrom<&'a str> for Jwt<'a, T>
|
||||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
type Error = JwtDecodeError;
|
||||
fn try_from(value: &'a str) -> Result<Self, Self::Error> {
|
||||
let raw = RawJwt::try_from(value)?;
|
||||
|
||||
let header =
|
||||
Base64UrlUnpadded::decode_vec(raw.header()).map_err(JwtDecodeError::decode_header)?;
|
||||
let header = serde_json::from_slice(&header).map_err(JwtDecodeError::deserialize_header)?;
|
||||
|
||||
let payload =
|
||||
Base64UrlUnpadded::decode_vec(raw.payload()).map_err(JwtDecodeError::decode_payload)?;
|
||||
let payload =
|
||||
serde_json::from_slice(&payload).map_err(JwtDecodeError::deserialize_payload)?;
|
||||
|
||||
let signature = Base64UrlUnpadded::decode_vec(raw.signature())
|
||||
.map_err(JwtDecodeError::decode_signature)?;
|
||||
|
||||
Ok(Self {
|
||||
raw,
|
||||
header,
|
||||
payload,
|
||||
signature,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JwtVerificationError {
|
||||
#[error("failed to parse signature")]
|
||||
ParseSignature {
|
||||
#[source]
|
||||
inner: signature::Error,
|
||||
},
|
||||
|
||||
#[error("signature verification failed")]
|
||||
Verify {
|
||||
#[source]
|
||||
inner: signature::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl JwtVerificationError {
|
||||
fn parse_signature(inner: signature::Error) -> Self {
|
||||
Self::ParseSignature { inner }
|
||||
}
|
||||
|
||||
fn verify(inner: signature::Error) -> Self {
|
||||
Self::Verify { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> Jwt<'a, T> {
|
||||
pub fn verify<K, S>(&self, key: &K) -> Result<(), JwtVerificationError>
|
||||
where
|
||||
K: Verifier<S>,
|
||||
S: Signature,
|
||||
{
|
||||
let signature =
|
||||
S::from_bytes(&self.signature).map_err(JwtVerificationError::parse_signature)?;
|
||||
|
||||
key.verify(self.raw.signed_part().as_bytes(), &signature)
|
||||
.map_err(JwtVerificationError::verify)
|
||||
}
|
||||
|
||||
pub fn as_str(&'a self) -> &'a str {
|
||||
&self.raw
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum JwtSignatureError {
|
||||
#[error("failed to serialize header")]
|
||||
EncodeHeader {
|
||||
#[source]
|
||||
inner: serde_json::Error,
|
||||
},
|
||||
|
||||
#[error("failed to serialize payload")]
|
||||
EncodePayload {
|
||||
#[source]
|
||||
inner: serde_json::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl JwtSignatureError {
|
||||
fn encode_header(inner: serde_json::Error) -> Self {
|
||||
Self::EncodeHeader { inner }
|
||||
}
|
||||
|
||||
fn encode_payload(inner: serde_json::Error) -> Self {
|
||||
Self::EncodePayload { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Jwt<'static, T> {
|
||||
pub fn sign<K, S>(
|
||||
header: JsonWebSignatureHeader,
|
||||
payload: T,
|
||||
key: &K,
|
||||
) -> Result<Self, JwtSignatureError>
|
||||
where
|
||||
K: Signer<S>,
|
||||
S: Signature,
|
||||
T: Serialize,
|
||||
{
|
||||
let header_ = serde_json::to_vec(&header).map_err(JwtSignatureError::encode_header)?;
|
||||
let header_ = Base64UrlUnpadded::encode_string(&header_);
|
||||
|
||||
let payload_ = serde_json::to_vec(&payload).map_err(JwtSignatureError::encode_payload)?;
|
||||
let payload_ = Base64UrlUnpadded::encode_string(&payload_);
|
||||
|
||||
let mut inner = format!("{}.{}", header_, payload_);
|
||||
|
||||
let first_dot = header_.len();
|
||||
let second_dot = inner.len();
|
||||
|
||||
let signature = key.sign(inner.as_bytes()).as_bytes().to_vec();
|
||||
let signature_ = Base64UrlUnpadded::encode_string(&signature);
|
||||
inner.reserve_exact(1 + signature_.len());
|
||||
inner.push('.');
|
||||
inner.push_str(&signature_);
|
||||
|
||||
let raw = RawJwt::new(inner, first_dot, second_dot);
|
||||
|
||||
Ok(Self {
|
||||
raw,
|
||||
header,
|
||||
payload,
|
||||
signature,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
use rand::thread_rng;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_jwt_decode() {
|
||||
let jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
|
||||
let jwt: Jwt<'_, serde_json::Value> = Jwt::try_from(jwt).unwrap();
|
||||
assert_eq!(jwt.raw.header(), "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
|
||||
assert_eq!(
|
||||
jwt.raw.payload(),
|
||||
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ"
|
||||
);
|
||||
assert_eq!(
|
||||
jwt.raw.signature(),
|
||||
"SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
|
||||
);
|
||||
assert_eq!(jwt.raw.signed_part(), "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_jwt_sign_and_verify() {
|
||||
let header = JsonWebSignatureHeader::new(JsonWebSignatureAlg::Es256);
|
||||
let payload = serde_json::json!({"hello": "world"});
|
||||
|
||||
let key = ecdsa::SigningKey::<p256::NistP256>::random(&mut thread_rng());
|
||||
let signed = Jwt::sign(header, payload, &key).unwrap();
|
||||
signed.verify(&key.verifying_key()).unwrap();
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ use tower::{
|
||||
};
|
||||
|
||||
use super::StaticJwksStore;
|
||||
use crate::{JsonWebKeySet, JwtHeader, VerifyingKeystore};
|
||||
use crate::{JsonWebKeySet, JsonWebSignatureHeader, VerifyingKeystore};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
@ -121,7 +121,12 @@ impl VerifyingKeystore for DynamicJwksStore {
|
||||
type Error = Error;
|
||||
type Future = BoxFuture<'static, Result<(), Self::Error>>;
|
||||
|
||||
fn verify(&self, header: &JwtHeader, payload: &[u8], signature: &[u8]) -> Self::Future {
|
||||
fn verify(
|
||||
&self,
|
||||
header: &JsonWebSignatureHeader,
|
||||
payload: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Self::Future {
|
||||
let cache = self.cache.clone();
|
||||
let exporter = self.exporter.clone();
|
||||
let header = header.clone();
|
||||
|
@ -21,7 +21,7 @@ use sha2::{Sha256, Sha384, Sha512};
|
||||
use signature::{Signature, Verifier};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{JsonWebKey, JsonWebKeySet, JwtHeader, VerifyingKeystore};
|
||||
use crate::{JsonWebKey, JsonWebKeySet, JsonWebSignatureHeader, VerifyingKeystore};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
@ -154,7 +154,7 @@ impl StaticJwksStore {
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn verify_sync(
|
||||
&self,
|
||||
header: &JwtHeader,
|
||||
header: &JsonWebSignatureHeader,
|
||||
payload: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
@ -227,7 +227,12 @@ impl VerifyingKeystore for StaticJwksStore {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<(), Self::Error>>;
|
||||
|
||||
fn verify(&self, header: &JwtHeader, payload: &[u8], signature: &[u8]) -> Self::Future {
|
||||
fn verify(
|
||||
&self,
|
||||
header: &JsonWebSignatureHeader,
|
||||
payload: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Self::Future {
|
||||
std::future::ready(self.verify_sync(header, payload, signature))
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ use sha2::{Sha256, Sha384, Sha512};
|
||||
use thiserror::Error;
|
||||
|
||||
use super::{SigningKeystore, VerifyingKeystore};
|
||||
use crate::JwtHeader;
|
||||
use crate::JsonWebSignatureHeader;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
@ -50,7 +50,7 @@ impl<'a> SharedSecret<'a> {
|
||||
|
||||
fn verify_sync(
|
||||
&self,
|
||||
header: &JwtHeader,
|
||||
header: &JsonWebSignatureHeader,
|
||||
payload: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Result<(), Error> {
|
||||
@ -92,7 +92,10 @@ impl<'a> SigningKeystore for SharedSecret<'a> {
|
||||
algorithms
|
||||
}
|
||||
|
||||
async fn prepare_header(&self, alg: JsonWebSignatureAlg) -> anyhow::Result<JwtHeader> {
|
||||
async fn prepare_header(
|
||||
&self,
|
||||
alg: JsonWebSignatureAlg,
|
||||
) -> anyhow::Result<JsonWebSignatureHeader> {
|
||||
if !matches!(
|
||||
alg,
|
||||
JsonWebSignatureAlg::Hs256 | JsonWebSignatureAlg::Hs384 | JsonWebSignatureAlg::Hs512,
|
||||
@ -100,10 +103,10 @@ impl<'a> SigningKeystore for SharedSecret<'a> {
|
||||
bail!("unsupported algorithm")
|
||||
}
|
||||
|
||||
Ok(JwtHeader::new(alg))
|
||||
Ok(JsonWebSignatureHeader::new(alg))
|
||||
}
|
||||
|
||||
async fn sign(&self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||
async fn sign(&self, header: &JsonWebSignatureHeader, 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() {
|
||||
@ -136,7 +139,12 @@ impl<'a> VerifyingKeystore for SharedSecret<'a> {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<(), Self::Error>>;
|
||||
|
||||
fn verify(&self, header: &JwtHeader, payload: &[u8], signature: &[u8]) -> Self::Future {
|
||||
fn verify(
|
||||
&self,
|
||||
header: &JsonWebSignatureHeader,
|
||||
payload: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Self::Future {
|
||||
std::future::ready(self.verify_sync(header, payload, signature))
|
||||
}
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ use signature::{Signature, Signer, Verifier};
|
||||
use tower::Service;
|
||||
|
||||
use super::{SigningKeystore, VerifyingKeystore};
|
||||
use crate::{JsonWebKey, JsonWebKeySet, JwtHeader};
|
||||
use crate::{JsonWebKey, JsonWebKeySet, JsonWebSignatureHeader};
|
||||
|
||||
// Generate with
|
||||
// openssl genrsa 2048
|
||||
@ -132,7 +132,7 @@ impl StaticKeystore {
|
||||
|
||||
fn verify_sync(
|
||||
&self,
|
||||
header: &JwtHeader,
|
||||
header: &JsonWebSignatureHeader,
|
||||
payload: &[u8],
|
||||
signature: &[u8],
|
||||
) -> anyhow::Result<()> {
|
||||
@ -245,8 +245,11 @@ impl SigningKeystore for StaticKeystore {
|
||||
algorithms
|
||||
}
|
||||
|
||||
async fn prepare_header(&self, alg: JsonWebSignatureAlg) -> anyhow::Result<JwtHeader> {
|
||||
let header = JwtHeader::new(alg);
|
||||
async fn prepare_header(
|
||||
&self,
|
||||
alg: JsonWebSignatureAlg,
|
||||
) -> anyhow::Result<JsonWebSignatureHeader> {
|
||||
let header = JsonWebSignatureHeader::new(alg);
|
||||
|
||||
let kid = match alg {
|
||||
JsonWebSignatureAlg::Rs256
|
||||
@ -267,7 +270,7 @@ impl SigningKeystore for StaticKeystore {
|
||||
Ok(header.with_kid(kid))
|
||||
}
|
||||
|
||||
async fn sign(&self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||
async fn sign(&self, header: &JsonWebSignatureHeader, msg: &[u8]) -> anyhow::Result<Vec<u8>> {
|
||||
let kid = header
|
||||
.kid()
|
||||
.ok_or_else(|| anyhow::anyhow!("missing kid from the JWT header"))?;
|
||||
@ -350,7 +353,12 @@ impl VerifyingKeystore for StaticKeystore {
|
||||
type Error = anyhow::Error;
|
||||
type Future = Ready<Result<(), Self::Error>>;
|
||||
|
||||
fn verify(&self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> Self::Future {
|
||||
fn verify(
|
||||
&self,
|
||||
header: &JsonWebSignatureHeader,
|
||||
msg: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Self::Future {
|
||||
std::future::ready(self.verify_sync(header, msg, signature))
|
||||
}
|
||||
}
|
||||
|
@ -22,22 +22,26 @@ use futures_util::{
|
||||
use mas_iana::jose::JsonWebSignatureAlg;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::JwtHeader;
|
||||
use crate::JsonWebSignatureHeader;
|
||||
|
||||
#[async_trait]
|
||||
pub trait SigningKeystore {
|
||||
fn supported_algorithms(&self) -> HashSet<JsonWebSignatureAlg>;
|
||||
|
||||
async fn prepare_header(&self, alg: JsonWebSignatureAlg) -> anyhow::Result<JwtHeader>;
|
||||
async fn prepare_header(
|
||||
&self,
|
||||
alg: JsonWebSignatureAlg,
|
||||
) -> anyhow::Result<JsonWebSignatureHeader>;
|
||||
|
||||
async fn sign(&self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result<Vec<u8>>;
|
||||
async fn sign(&self, header: &JsonWebSignatureHeader, msg: &[u8]) -> anyhow::Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub trait VerifyingKeystore {
|
||||
type Error;
|
||||
type Future: Future<Output = Result<(), Self::Error>>;
|
||||
|
||||
fn verify(&self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> Self::Future;
|
||||
fn verify(&self, header: &JsonWebSignatureHeader, msg: &[u8], signature: &[u8])
|
||||
-> Self::Future;
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@ -61,7 +65,12 @@ where
|
||||
MapErr<R::Future, fn(R::Error) -> Self::Error>,
|
||||
>;
|
||||
|
||||
fn verify(&self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> Self::Future {
|
||||
fn verify(
|
||||
&self,
|
||||
header: &JsonWebSignatureHeader,
|
||||
msg: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Self::Future {
|
||||
match self {
|
||||
Either::Left(left) => Either::Left(
|
||||
left.verify(header, msg, signature)
|
||||
@ -83,7 +92,12 @@ where
|
||||
type Error = T::Error;
|
||||
type Future = T::Future;
|
||||
|
||||
fn verify(&self, header: &JwtHeader, msg: &[u8], signature: &[u8]) -> Self::Future {
|
||||
fn verify(
|
||||
&self,
|
||||
header: &JsonWebSignatureHeader,
|
||||
msg: &[u8],
|
||||
signature: &[u8],
|
||||
) -> Self::Future {
|
||||
self.as_ref().verify(header, msg, signature)
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ pub use futures_util::future::Either;
|
||||
|
||||
pub use self::{
|
||||
jwk::{JsonWebKey, JsonWebKeySet},
|
||||
jwt::{DecodedJsonWebToken, JsonWebTokenParts, JwtHeader},
|
||||
jwt::{DecodedJsonWebToken, JsonWebSignatureHeader, JsonWebTokenParts, Jwt, JwtSignatureError},
|
||||
keystore::{
|
||||
DynamicJwksStore, SharedSecret, SigningKeystore, StaticJwksStore, StaticKeystore,
|
||||
VerifyingKeystore,
|
||||
|
Reference in New Issue
Block a user