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

Add tests for private_key_jwt client auth

This commit is contained in:
Quentin Gliech
2022-01-06 10:04:43 +01:00
parent ef3da801a3
commit f0a7e96875
3 changed files with 181 additions and 62 deletions

View File

@ -61,6 +61,12 @@ impl JwksOrJwksUri {
}
}
impl From<JsonWebKeySet> for JwksOrJwksUri {
fn from(jwks: JsonWebKeySet) -> Self {
Self::Jwks(jwks)
}
}
#[derive(JsonSchema, Serialize, Deserialize, Clone, Debug)]
#[serde(tag = "client_auth_method", rename_all = "snake_case")]
pub enum OAuth2ClientAuthMethodConfig {

View File

@ -18,21 +18,59 @@ use anyhow::bail;
use async_trait::async_trait;
use base64ct::{Base64UrlUnpadded, Encoding};
use digest::Digest;
use ecdsa::VerifyingKey;
use ecdsa::{SigningKey, VerifyingKey};
use p256::{NistP256, PublicKey};
use pkcs1::EncodeRsaPublicKey;
use pkcs8::EncodePublicKey;
use rsa::{PublicKey as _, RsaPublicKey};
use pkcs1::{DecodeRsaPrivateKey, EncodeRsaPublicKey};
use pkcs8::{DecodePrivateKey, EncodePublicKey};
use rsa::{PublicKey as _, RsaPrivateKey, RsaPublicKey};
use sha2::{Sha256, Sha384, Sha512};
use signature::{Signature, Signer, Verifier};
use super::{ExportJwks, SigningKeystore, VerifyingKeystore};
use crate::{iana::JsonWebSignatureAlgorithm, JsonWebKey, JsonWebKeySet, JwtHeader};
// Generate with
// openssl genrsa 2048
const TEST_RSA_PKCS1_PEM: &str = "-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA1j7Y2CH6Ss8tgaNvcQPaRJKnCZD8ABqNPyKDWLQLph6Zi7gZ
GqmRtTzMuevo2ezpkbCiQAPEp1ms022P92bB+uqG7xmzHTzbwLtnq3OAdjmrnaFV
I4v89WHUsTXX9hiYOK5dOM81bNZ6muxWZ0L/xw4jVWe7xkqnp2Lluq0HknlzP5yJ
UEikf5BkpX0iyIu2/X4r8YVp8uzG34l/8qBx6k3rO2VkOQOSybZj1oij5KZCusnu
QjJLKWXCqJToWE6iVn+Q0N6ySDLgmJ7Zq0Sou/9N/oWKn94FOsouQgET5NuzoIFR
qTb321fQ8gbqt/OupBbBKEo1qUU+cS77TD/AuQIDAQABAoIBAQDLSZzmD+93lnf+
f36ZxOcRk/nNGPYUfx0xH+VzgHthJ73YFlozs1xflQ5JB/DM/4BsziZWCX1KsctM
XrRxMt6y4GAidcc/4eQ+T1RCGfl1tKkDi/bGIOloSGjRsV5208V0WvZ3lh2CZUy2
vbQKjUc3sFGUkzZYI7RLHosPA2mg78IVuSnqvNaU0TgA2KkaxWs6Ecr/ys80cUvj
KKj04DmX5xaXwUKmz353i5gIt3aY3G5CAw5fU/ocDKR8nzVCpBAGbRRiUaVKIT06
APSkLDTUnxSYtHtDJGHjgU/TsvAwTA92J3ue5Ysu9xTE+WyHA6Rgux7RQSD/wWHr
LdRPwxPFAoGBAOytMPh/f2zKmotanjho0QNfhAUHoQUfPudYT0nnDceOsi1jYWbQ
c/wPeQQC4Hp/pTUrkSIQPEz/hSxzZ6RPxxuGB8O94I0uLwQK4V1UwbgfsRa9zQzW
n0kgKZ8w8h8B7qyiKyIAnZzvKtNEnKrzrct4HsN3OEoXTwuAUYlvWtQTAoGBAOe8
0liNaH9V6ecZiojkRR1tiQkr/dCV+a13eXXaRA/8y/3wKCQ4idYncclQJTLKsAwW
hHuDd4uLgtifREVIBD2jGdlznNr9HQNuZgwjuUoH+r1YLGgiMWeVYSr0m8lyDlQl
BJKTAphrqo6VJWDAnM18v+by//yRleSjVMqZ3zmDAoGBAMpA0rl5EyagGON/g/hG
sl8Ej+hQdazP38yJbfCEsATaD6+z3rei6Yr8mfjwkG5+iGrgmT0XzMAsF909ndMP
jeIabqY6rBtZ3TnCJobAeG9lPctmVUVkX2h5QLhWdoJC/3iteNis2AQVam5yksOQ
S/O16ew2BHdkZds5Q/SDoYXbAoGAK9tVZ8LjWu30hXMU/9FLr0USoTS9JWOszAKH
byFuriPmq1lvD2PP2kK+yx2q3JD1fmQokIOR9Uvi6IJD1mTJwKyEcN3reppailKz
Z2q/X15hOsJcLR0DgpoHuKxwa1B1m8Ehu2etHxGJRtC9MTFiu5T3cIrenXskBhBP
NMSoNWcCgYAD3u3zdeVo3gVoxneS7GNVI2WBhjtqgNIbINuxGZvfztm7+vNPE6sQ
VL8i+09uoM1H6sXbe2XXORmtW0j/6MmYhSoBXNdqWTNAiyNRhwEQtowqgl5R7PBu
//QZTF1z62R9IKDMRG3f5Wn8e1Dys6tXBuG603g+Dkkc/km476mrgw==
-----END RSA PRIVATE KEY-----";
// Generate with
// openssl ecparam -genkey -name prime256v1 | openssl pkcs8 -topk8 -nocrypt
const TEST_ECDSA_PKCS8_PEM: &str = "-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg+tHxet7G+uar2Cef
iYPb7jv3uzncFtwJ7RhDOvEA0fChRANCAATCKn2AEqa9785k+TmwkeCvLub8XGrF
ezE6bA/blaPVE3nu4SUVYKULRJQxNjeOSra8TQrlIS8e5ItbMn8Tv9KV
-----END PRIVATE KEY-----";
#[derive(Default)]
pub struct StaticKeystore {
rsa_keys: HashMap<String, rsa::RsaPrivateKey>,
es256_keys: HashMap<String, ecdsa::SigningKey<NistP256>>,
es256_keys: HashMap<String, SigningKey<NistP256>>,
}
impl StaticKeystore {
@ -41,6 +79,18 @@ impl StaticKeystore {
StaticKeystore::default()
}
pub fn add_test_rsa_key(&mut self) -> anyhow::Result<()> {
let rsa = RsaPrivateKey::from_pkcs1_pem(TEST_RSA_PKCS1_PEM)?;
self.add_rsa_key(rsa)?;
Ok(())
}
pub fn add_test_ecdsa_key(&mut self) -> anyhow::Result<()> {
let ecdsa = SigningKey::from_pkcs8_pem(TEST_ECDSA_PKCS8_PEM)?;
self.add_ecdsa_key(ecdsa)?;
Ok(())
}
pub fn add_rsa_key(&mut self, key: rsa::RsaPrivateKey) -> anyhow::Result<()> {
let pubkey: &RsaPublicKey = &key;
let der = pubkey.to_pkcs1_der()?;
@ -57,7 +107,7 @@ impl StaticKeystore {
Ok(())
}
pub fn add_ecdsa_key(&mut self, key: ecdsa::SigningKey<NistP256>) -> anyhow::Result<()> {
pub fn add_ecdsa_key(&mut self, key: SigningKey<NistP256>) -> anyhow::Result<()> {
let pubkey: PublicKey = key.verifying_key().into();
let der = EncodePublicKey::to_public_key_der(&pubkey)?;
let digest = {
@ -297,63 +347,15 @@ impl ExportJwks for StaticKeystore {
#[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_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.add_test_rsa_key().unwrap();
s.add_test_ecdsa_key().unwrap();
s
};

View File

@ -284,7 +284,7 @@ struct ClientAuthForm<T> {
mod tests {
use headers::authorization::Credentials;
use mas_config::{ConfigurationSection, OAuth2ClientAuthMethodConfig};
use mas_jose::{JsonWebSignatureAlgorithm, SigningKeystore};
use mas_jose::{ExportJwks, JsonWebSignatureAlgorithm, SigningKeystore, StaticKeystore};
use serde_json::json;
use super::*;
@ -292,7 +292,14 @@ mod tests {
// Long client_secret to support it as a HS512 key
const CLIENT_SECRET: &str = "leek2zaeyeb8thai7piehea3vah6ool9oanin9aeraThuci9EeghaekaiD1upe4Quoh7xeMae2meitohj0Waaveiwaorah1yazohr6Vae7iebeiRaWene5IeWeeciezu";
fn oauth2_config() -> OAuth2Config {
fn client_private_keystore() -> StaticKeystore {
let mut store = StaticKeystore::new();
store.add_test_rsa_key().unwrap();
store.add_test_ecdsa_key().unwrap();
store
}
async fn oauth2_config() -> OAuth2Config {
let mut config = OAuth2Config::test();
config.clients.push(OAuth2ClientConfig {
client_id: "public".to_string(),
@ -327,6 +334,19 @@ mod tests {
},
redirect_uris: Vec::new(),
});
let store = client_private_keystore();
let jwks = store.export_jwks().await.unwrap();
config.clients.push(OAuth2ClientConfig {
client_id: "private-key-jwt".to_string(),
client_auth_method: OAuth2ClientAuthMethodConfig::PrivateKeyJwt(jwks.clone().into()),
redirect_uris: Vec::new(),
});
config.clients.push(OAuth2ClientConfig {
client_id: "private-key-jwt-2".to_string(),
client_auth_method: OAuth2ClientAuthMethodConfig::PrivateKeyJwt(jwks.into()),
redirect_uris: Vec::new(),
});
config
}
@ -353,7 +373,7 @@ mod tests {
async fn client_secret_jwt(alg: JsonWebSignatureAlgorithm) {
let audience = "https://example.com/token".to_string();
let filter = client_authentication::<Form>(&oauth2_config(), audience.clone());
let filter = client_authentication::<Form>(&oauth2_config().await, audience.clone());
let store = SharedSecret::new(&CLIENT_SECRET);
let claims = ClientAssertionClaims {
@ -422,10 +442,101 @@ mod tests {
assert!(res.is_err());
}
#[tokio::test]
async fn client_secret_jwt_rs256() {
private_key_jwt(JsonWebSignatureAlgorithm::Rs256).await;
}
#[tokio::test]
async fn client_secret_jwt_rs384() {
private_key_jwt(JsonWebSignatureAlgorithm::Rs384).await;
}
#[tokio::test]
async fn client_secret_jwt_rs512() {
private_key_jwt(JsonWebSignatureAlgorithm::Rs512).await;
}
#[tokio::test]
async fn client_secret_jwt_es256() {
private_key_jwt(JsonWebSignatureAlgorithm::Es256).await;
}
async fn private_key_jwt(alg: JsonWebSignatureAlgorithm) {
let audience = "https://example.com/token".to_string();
let filter = client_authentication::<Form>(&oauth2_config().await, audience.clone());
let store = client_private_keystore();
let claims = ClientAssertionClaims {
issuer: "private-key-jwt".to_string(),
subject: "private-key-jwt".to_string(),
audience,
jwt_id: None,
};
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
// - expired token
// - "not before" in the future
// - subject/issuer mismatch
// - audience mismatch
// - wrong secret/signature
let (auth, client, body) = warp::test::request()
.method("POST")
.header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string())
.body(serde_urlencoded::to_string(json!({
"client_id": "private-key-jwt",
"client_assertion": jwt,
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"foo": "baz",
"bar": "foobar",
})).unwrap())
.filter(&filter)
.await
.unwrap();
assert_eq!(auth, ClientAuthenticationMethod::PrivateKeyJwt);
assert_eq!(client.client_id, "private-key-jwt");
assert_eq!(body.foo, "baz");
assert_eq!(body.bar, "foobar");
// Without client_id
let res = warp::test::request()
.method("POST")
.header("Content-Type", mime::APPLICATION_WWW_FORM_URLENCODED.to_string())
.body(serde_urlencoded::to_string(json!({
"client_assertion": jwt,
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"foo": "baz",
"bar": "foobar",
})).unwrap())
.filter(&filter)
.await;
assert!(res.is_ok());
// client_id mismatch
let res = warp::test::request()
.method("POST")
.body(serde_urlencoded::to_string(json!({
"client_id": "private-key-jwt-2",
"client_assertion": jwt,
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"foo": "baz",
"bar": "foobar",
})).unwrap())
.filter(&filter)
.await;
assert!(res.is_err());
}
#[tokio::test]
async fn client_secret_post() {
let filter = client_authentication::<Form>(
&oauth2_config(),
&oauth2_config().await,
"https://example.com/token".to_string(),
);
@ -457,7 +568,7 @@ mod tests {
#[tokio::test]
async fn client_secret_basic() {
let filter = client_authentication::<Form>(
&oauth2_config(),
&oauth2_config().await,
"https://example.com/token".to_string(),
);
@ -489,7 +600,7 @@ mod tests {
#[tokio::test]
async fn none() {
let filter = client_authentication::<Form>(
&oauth2_config(),
&oauth2_config().await,
"https://example.com/token".to_string(),
);