diff --git a/crates/jose/src/keystore/mod.rs b/crates/jose/src/keystore/mod.rs new file mode 100644 index 00000000..4203db4b --- /dev/null +++ b/crates/jose/src/keystore/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod shared_secret; +mod static_keystore; +mod traits; + +pub use self::{ + shared_secret::SharedSecret, + static_keystore::StaticKeystore, + traits::{ExportJwks, SigningKeystore, VerifyingKeystore}, +}; diff --git a/crates/jose/src/keystore/shared_secret.rs b/crates/jose/src/keystore/shared_secret.rs new file mode 100644 index 00000000..9f4467e2 --- /dev/null +++ b/crates/jose/src/keystore/shared_secret.rs @@ -0,0 +1,134 @@ +// 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 anyhow::bail; +use async_trait::async_trait; +use hmac::{Hmac, Mac}; +use sha2::{Sha256, Sha384, Sha512}; + +use super::{SigningKeystore, VerifyingKeystore}; +use crate::{JsonWebSignatureAlgorithm, JwtHeader}; + +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 { + 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> { + // 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::::new_from_slice(self.inner)?; + mac.update(msg); + mac.finalize().into_bytes().to_vec() + } + + JsonWebSignatureAlgorithm::Hs384 => { + let mut mac = Hmac::::new_from_slice(self.inner)?; + mac.update(msg); + mac.finalize().into_bytes().to_vec() + } + + JsonWebSignatureAlgorithm::Hs512 => { + let mut mac = Hmac::::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::::new_from_slice(self.inner)?; + mac.update(payload); + mac.verify(signature.try_into()?)?; + } + + JsonWebSignatureAlgorithm::Hs384 => { + let mut mac = Hmac::::new_from_slice(self.inner)?; + mac.update(payload); + mac.verify(signature.try_into()?)?; + } + + JsonWebSignatureAlgorithm::Hs512 => { + let mut mac = Hmac::::new_from_slice(self.inner)?; + mac.update(payload); + mac.verify(signature.try_into()?)?; + } + + _ => bail!("unsupported algorithm"), + }; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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(); + } + } +} diff --git a/crates/jose/src/keystore.rs b/crates/jose/src/keystore/static_keystore.rs similarity index 76% rename from crates/jose/src/keystore.rs rename to crates/jose/src/keystore/static_keystore.rs index 98dbf4f6..1fb2bdb6 100644 --- a/crates/jose/src/keystore.rs +++ b/crates/jose/src/keystore/static_keystore.rs @@ -19,7 +19,6 @@ use async_trait::async_trait; use base64ct::{Base64UrlUnpadded, Encoding}; use digest::Digest; use ecdsa::VerifyingKey; -use hmac::{Hmac, Mac}; use p256::{NistP256, PublicKey}; use pkcs1::EncodeRsaPublicKey; use pkcs8::EncodePublicKey; @@ -27,118 +26,12 @@ use rsa::{PublicKey as _, RsaPublicKey}; use sha2::{Sha256, Sha384, Sha512}; use signature::{Signature, Signer, Verifier}; +use super::{ExportJwks, SigningKeystore, VerifyingKeystore}; use crate::{ - iana::JsonWebSignatureAlgorithm, JsonWebKey, JsonWebKeyOperation, JsonWebKeySet, JwtHeader, + iana::{JsonWebKeyOperation, JsonWebSignatureAlgorithm}, + JsonWebKey, JsonWebKeySet, JwtHeader, }; -#[async_trait] -pub trait SigningKeystore { - async fn prepare_header(self, alg: JsonWebSignatureAlgorithm) -> anyhow::Result; - - async fn sign(self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result>; -} - -#[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 { - 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> { - // 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::::new_from_slice(self.inner)?; - mac.update(msg); - mac.finalize().into_bytes().to_vec() - } - - JsonWebSignatureAlgorithm::Hs384 => { - let mut mac = Hmac::::new_from_slice(self.inner)?; - mac.update(msg); - mac.finalize().into_bytes().to_vec() - } - - JsonWebSignatureAlgorithm::Hs512 => { - let mut mac = Hmac::::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::::new_from_slice(self.inner)?; - mac.update(payload); - mac.verify(signature.try_into()?)?; - } - - JsonWebSignatureAlgorithm::Hs384 => { - let mut mac = Hmac::::new_from_slice(self.inner)?; - mac.update(payload); - mac.verify(signature.try_into()?)?; - } - - JsonWebSignatureAlgorithm::Hs512 => { - let mut mac = Hmac::::new_from_slice(self.inner)?; - mac.update(payload); - mac.verify(signature.try_into()?)?; - } - - _ => bail!("unsupported algorithm"), - }; - - Ok(()) - } -} - #[derive(Default)] pub struct StaticKeystore { rsa_keys: HashMap, @@ -463,23 +356,6 @@ 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(); diff --git a/crates/jose/src/keystore/traits.rs b/crates/jose/src/keystore/traits.rs new file mode 100644 index 00000000..2343c3df --- /dev/null +++ b/crates/jose/src/keystore/traits.rs @@ -0,0 +1,37 @@ +// 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 async_trait::async_trait; + +use crate::{ + iana::JsonWebSignatureAlgorithm, JsonWebKeySet, JwtHeader, +}; + +#[async_trait] +pub trait SigningKeystore { + async fn prepare_header(self, alg: JsonWebSignatureAlgorithm) -> anyhow::Result; + + async fn sign(self, header: &JwtHeader, msg: &[u8]) -> anyhow::Result>; +} + +#[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; +} +