diff --git a/v-api-param/src/lib.rs b/v-api-param/src/lib.rs index 2debb58..842ba82 100644 --- a/v-api-param/src/lib.rs +++ b/v-api-param/src/lib.rs @@ -54,11 +54,16 @@ impl StringParam { /// /// For inline values, returns the value directly. /// For path-based values, reads the file contents and trims trailing whitespace. - pub fn resolve(&self) -> Result { + pub fn resolve(&self, base: Option) -> Result { match self { StringParam::Inline(value) => Ok(value.clone()), StringParam::FromPath { path } => { - let content = std::fs::read_to_string(path).map_err(|source| { + let path = if let Some(base) = base { + base.join(path) + } else { + path.clone() + }; + let content = std::fs::read_to_string(&path).map_err(|source| { ParamResolutionError::FileRead { path: path.display().to_string(), source, @@ -93,7 +98,7 @@ mod tests { #[test] fn test_inline_value() { let param = StringParam::Inline("my-param".to_string().into()); - assert_eq!(param.resolve().unwrap().expose_secret(), "my-param"); + assert_eq!(param.resolve(None).unwrap().expose_secret(), "my-param"); } #[test] @@ -104,7 +109,23 @@ mod tests { let param = StringParam::FromPath { path: file.path().to_path_buf(), }; - assert_eq!(param.resolve().unwrap().expose_secret(), "file-param"); + assert_eq!(param.resolve(None).unwrap().expose_secret(), "file-param"); + } + + #[test] + fn test_from_path_with_base() { + let mut file = NamedTempFile::new().unwrap(); + write!(file, "file-param").unwrap(); + + let param = StringParam::FromPath { + path: PathBuf::from(file.path().file_name().unwrap()), + }; + let base_path = std::env::temp_dir(); + + assert_eq!( + param.resolve(Some(base_path)).unwrap().expose_secret(), + "file-param" + ); } #[test] @@ -116,7 +137,7 @@ mod tests { let param = StringParam::FromPath { path: file.path().to_path_buf(), }; - assert_eq!(param.resolve().unwrap().expose_secret(), "file-param"); + assert_eq!(param.resolve(None).unwrap().expose_secret(), "file-param"); } #[test] @@ -124,7 +145,7 @@ mod tests { let param = StringParam::FromPath { path: PathBuf::from("/nonexistent/path"), }; - let result = param.resolve(); + let result = param.resolve(None); assert!(matches!(result, Err(ParamResolutionError::FileRead { .. }))); } @@ -139,7 +160,7 @@ mod tests { let config: Config = toml::from_str(toml).unwrap(); assert_eq!( - config.key.resolve().unwrap().expose_secret(), + config.key.resolve(None).unwrap().expose_secret(), "inline-value" ); } @@ -157,6 +178,9 @@ mod tests { } let config: Config = toml::from_str(&toml).unwrap(); - assert_eq!(config.key.resolve().unwrap().expose_secret(), "path-value"); + assert_eq!( + config.key.resolve(None).unwrap().expose_secret(), + "path-value" + ); } } diff --git a/v-api/src/authn/jwt.rs b/v-api/src/authn/jwt.rs index d138bba..8733f00 100644 --- a/v-api/src/authn/jwt.rs +++ b/v-api/src/authn/jwt.rs @@ -2,29 +2,25 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use std::{fmt::Debug, sync::Arc}; - use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use chrono::{DateTime, Utc}; use jsonwebtoken::{ decode, decode_header, - jwk::{ - AlgorithmParameters, CommonParameters, Jwk, KeyAlgorithm, PublicKeyUse, RSAKeyParameters, - RSAKeyType, - }, + jwk::{AlgorithmParameters, Jwk}, Algorithm, DecodingKey, Header, Validation, }; use newtype_uuid::TypedUuid; -use rsa::traits::PublicKeyParts; use serde::{Deserialize, Serialize}; -use tap::TapFallible; +use std::{fmt::Debug, sync::Arc}; use thiserror::Error; use tracing::instrument; use v_model::{AccessTokenId, UserId, UserProviderId}; -use crate::{config::AsymmetricKey, context::VContext, permissions::VAppPermission}; +use crate::{authn::Signer, context::VContext, permissions::VAppPermission}; -use super::{Signer, SigningKeyError}; +use super::SigningKeyError; + +pub static DEFAULT_JWT_EXPIRATION: i64 = 3600; #[derive(Debug, Error)] pub enum JwtError { @@ -153,20 +149,15 @@ pub struct JwtSigner { #[allow(dead_code)] header: Header, encoded_header: String, - signer: Arc, + signer: Arc, } impl JwtSigner { - pub fn new(key: &AsymmetricKey) -> Result { + pub fn new(signer: Arc) -> Result { let mut header = Header::new(Algorithm::RS256); - header.kid = Some(key.kid().to_string()); + header.kid = Some(signer.kid.clone()); let encoded_header = to_base64_json(&header)?; - let signer = key - .as_signer() - .map_err(JwtSignerError::InvalidKey) - .tap_err(|err| tracing::error!(?err, "Unable to construct signer for JWT key"))?; - Ok(Self { header, encoded_header, @@ -200,31 +191,6 @@ impl JwtSigner { } } -impl AsymmetricKey { - pub fn as_jwk(&self) -> Result { - let key_id = self.kid(); - let public_key = self.public_key().map_err(JwtSignerError::InvalidKey)?; - - Ok(Jwk { - common: CommonParameters { - public_key_use: Some(PublicKeyUse::Signature), - key_operations: None, - key_algorithm: Some(KeyAlgorithm::RS256), - key_id: Some(key_id.to_string()), - x509_chain: None, - x509_sha1_fingerprint: None, - x509_sha256_fingerprint: None, - x509_url: None, - }, - algorithm: AlgorithmParameters::RSA(RSAKeyParameters { - key_type: RSAKeyType::RSA, - n: URL_SAFE_NO_PAD.encode(public_key.n().to_bytes_be()), - e: URL_SAFE_NO_PAD.encode(public_key.e().to_bytes_be()), - }), - }) - } -} - fn to_base64_json(value: &T) -> Result where T: Serialize, diff --git a/v-api/src/authn/key.rs b/v-api/src/authn/key.rs index 9e76a44..b2f3095 100644 --- a/v-api/src/authn/key.rs +++ b/v-api/src/authn/key.rs @@ -8,9 +8,9 @@ use secrecy::{ExposeSecret, SecretSlice, SecretString}; use thiserror::Error; use uuid::Uuid; -use crate::authn::Verifier; +use crate::authn::{Sign, Verify}; -use super::{Signer, SigningKeyError}; +use super::SigningKeyError; pub struct RawKey { clear: SecretSlice, @@ -49,7 +49,7 @@ impl RawKey { &self.clear.expose_secret()[0..16] } - pub async fn sign(self, signer: &dyn Signer) -> Result { + pub async fn sign(self, signer: &dyn Sign) -> Result { let signature = hex::encode( signer .sign(self.clear.expose_secret()) @@ -64,7 +64,7 @@ impl RawKey { pub fn verify(&self, verifier: &T, signature: &[u8]) -> Result<(), ApiKeyError> where - T: Verifier, + T: Verify, { let signature = hex::decode(signature)?; if verifier @@ -143,15 +143,15 @@ mod tests { use super::RawKey; use crate::{ - authn::{VerificationResult, Verifier}, + authn::{VerificationResult, Verify}, util::tests::{mock_key, MockKey}, }; struct TestVerifier { - verifier: Arc, + verifier: Arc, } - impl Verifier for TestVerifier { + impl Verify for TestVerifier { fn verify(&self, message: &[u8], signature: &[u8]) -> VerificationResult { self.verifier.verify(message, signature) } @@ -161,13 +161,13 @@ mod tests { async fn test_verifies_signature() { let id = Uuid::new_v4(); let MockKey { signer, verifier } = mock_key("test"); - let signer = signer.as_signer().unwrap(); + let signer = signer.resolve_signer(None).unwrap(); let verifier = TestVerifier { - verifier: verifier.as_verifier().unwrap(), + verifier: Arc::new(verifier.resolve_verifier(None).await.unwrap()), }; let raw = RawKey::generate::<8>(&id); - let signed = raw.sign(&*signer).await.unwrap(); + let signed = raw.sign(&signer).await.unwrap(); let raw2 = RawKey::try_from(signed.key.expose_secret()).unwrap(); @@ -181,13 +181,13 @@ mod tests { async fn test_generates_signatures() { let id = Uuid::new_v4(); let MockKey { signer, .. } = mock_key("test"); - let signer = signer.as_signer().unwrap(); + let signer = signer.resolve_signer(None).unwrap(); let raw1 = RawKey::generate::<8>(&id); - let signed1 = raw1.sign(&*signer).await.unwrap(); + let signed1 = raw1.sign(&signer).await.unwrap(); let raw2 = RawKey::generate::<8>(&id); - let signed2 = raw2.sign(&*signer).await.unwrap(); + let signed2 = raw2.sign(&signer).await.unwrap(); assert_ne!(signed1.signature(), signed2.signature()) } diff --git a/v-api/src/authn/mod.rs b/v-api/src/authn/mod.rs index ea33d33..9820a85 100644 --- a/v-api/src/authn/mod.rs +++ b/v-api/src/authn/mod.rs @@ -7,7 +7,6 @@ use base64::{prelude::BASE64_STANDARD, Engine}; use crc32c::crc32c; use dropshot::{HttpError, RequestContext, SharedExtractor}; use dropshot_authorization_header::bearer::BearerAuth; -use futures::executor::block_on; use google_cloudkms1::{ api::AsymmetricSignRequest, hyper_rustls::HttpsConnector, hyper_util::client::legacy::connect::HttpConnector, CloudKMS, @@ -15,25 +14,18 @@ use google_cloudkms1::{ use rsa::{ pkcs1v15::Signature, pkcs1v15::{SigningKey, VerifyingKey}, - pkcs8::{DecodePrivateKey, DecodePublicKey}, signature::{RandomizedSigner, SignatureEncoding, Verifier as RsaVerifier}, - RsaPrivateKey, RsaPublicKey, }; -use secrecy::ExposeSecret; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; -use std::{fmt::Debug, sync::Arc}; +use std::fmt::Debug; use thiserror::Error; -use tracing::instrument; use v_api_param::ParamResolutionError; use v_model::permissions::PermissionStorage; use crate::{ - authn::key::RawKey, - config::AsymmetricKey, - context::ApiContext, - permissions::VAppPermission, - util::{cloud_kms_client, response::unauthorized}, + authn::key::RawKey, context::ApiContext, permissions::VAppPermission, + util::response::unauthorized, }; use self::jwt::Jwt; @@ -136,11 +128,6 @@ pub enum SigningKeyError { Signature(#[from] rsa::signature::Error), } -#[async_trait] -pub trait Signer: Send + Sync { - async fn sign(&self, message: &[u8]) -> Result, SigningKeyError>; -} - #[derive(Debug, Default)] pub struct VerificationResult { pub verified: bool, @@ -148,7 +135,12 @@ pub struct VerificationResult { } #[async_trait] -pub trait Verifier: Send + Sync { +pub trait Sign: Send + Sync { + async fn sign(&self, message: &[u8]) -> Result, SigningKeyError>; +} + +#[async_trait] +pub trait Verify: Send + Sync { fn verify(&self, message: &[u8], signature: &[u8]) -> VerificationResult; } @@ -157,65 +149,20 @@ pub struct LocalSigningKey { signing_key: SigningKey, } -// A signer that stores a local in memory key for verifying JWTs -pub struct LocalVerifyingKey { - verifying_key: VerifyingKey, -} - -#[async_trait] -impl Signer for LocalSigningKey { - #[instrument(skip(self, message), err(Debug))] - async fn sign(&self, message: &[u8]) -> Result, SigningKeyError> { - tracing::trace!("Signing message"); - let mut rng = rand::thread_rng(); - let signature = self.signing_key.sign_with_rng(&mut rng, message).to_vec(); - - Ok(signature) - } -} - -#[async_trait] -impl Verifier for LocalVerifyingKey { - fn verify(&self, message: &[u8], signature: &[u8]) -> VerificationResult { - let signature = Signature::try_from(signature); - tracing::trace!("Verifying message"); - match signature { - Ok(signature) => { - let verification_result = self.verifying_key.verify(message, &signature); - match verification_result { - Ok(()) => VerificationResult { - verified: true, - errors: vec![None], - }, - Err(err) => VerificationResult { - verified: false, - errors: vec![Some(SigningKeyError::from(err))], - }, - } - } - Err(err) => VerificationResult { - verified: false, - errors: vec![Some(SigningKeyError::from(err))], - }, - } +impl LocalSigningKey { + pub fn new(signing_key: SigningKey) -> Self { + Self { signing_key } } } -impl Verifier for Arc -where - T: Verifier, -{ - fn verify(&self, message: &[u8], signature: &[u8]) -> VerificationResult { - (**self).verify(message, signature) - } +// A signer that stores a local in memory key for verifying JWTs +pub struct LocalVerifyingKey { + verifying_key: VerifyingKey, } -impl Verifier for &Arc -where - T: Verifier, -{ - fn verify(&self, message: &[u8], signature: &[u8]) -> VerificationResult { - (**self).verify(message, signature) +impl LocalVerifyingKey { + pub fn new(verifying_key: VerifyingKey) -> Self { + Self { verifying_key } } } @@ -246,11 +193,23 @@ pub struct CloudKmsSigningKey { key_name: String, } +impl CloudKmsSigningKey { + pub fn new(client: CloudKMS>, key_name: String) -> Self { + Self { client, key_name } + } +} + // Verifier that fetches and stores a public key from Cloud KMS. pub struct CloudKmsVerifyingKey { verifying_key: VerifyingKey, } +impl CloudKmsVerifyingKey { + pub fn new(verifying_key: VerifyingKey) -> Self { + Self { verifying_key } + } +} + // A fallback type for deserializing signature responses. google-cloudkms1 currently fails to decode // the base64 signature due to assuming it to be url safe #[derive(Debug, Serialize, Deserialize)] @@ -262,204 +221,274 @@ pub struct CloudKmsSignatureResponse { pub signature_crc32c: String, } -#[async_trait] -impl Signer for CloudKmsSigningKey { - #[instrument(skip(self, message), err(Debug))] - async fn sign(&self, message: &[u8]) -> Result, SigningKeyError> { - let mut hasher = Sha256::new(); - hasher.update(message); - let digest = hasher.finalize(); - - let check = crc32c(&digest); - - let response = self - .client - .projects() - .locations_key_rings_crypto_keys_crypto_key_versions_asymmetric_sign( - AsymmetricSignRequest { - data: None, - data_crc32c: None, - digest: Some(google_cloudkms1::api::Digest { - sha256: Some(digest.to_vec()), - sha384: None, - sha512: None, - }), - digest_crc32c: Some(check as i64), - }, - &self.key_name, - ) - .doit() - .await; - - tracing::info!("Received response from remote signer"); - - let signature = match response { - Ok((_, response)) => { - tracing::info!("Library deserialization succeeded"); - response - .signature - .ok_or_else(|| Box::new(CloudKmsError::MissingSignature)) +pub struct Signer { + pub kid: String, + pub key: SignerKey, +} + +impl Signer { + pub fn new(kid: String, key: SignerKey) -> Self { + Self { kid, key } + } +} + +pub enum SignerKey { + Local(LocalSigningKey), + Ckms(CloudKmsSigningKey), +} + +impl Signer { + pub async fn sign(&self, message: &[u8]) -> Result, SigningKeyError> { + match &self.key { + SignerKey::Local(local) => { + tracing::trace!("Signing message"); + let mut rng = rand::thread_rng(); + let signature = local.signing_key.sign_with_rng(&mut rng, message).to_vec(); + + Ok(signature) } - Err(google_cloudkms1::Error::JsonDecodeError(body, _)) => { - tracing::info!("Using fallback deserialization"); - serde_json::from_str::(&body) - .map_err(CloudKmsError::FailedToDeserialize) - .map_err(Box::new) - .and_then(|resp| { - BASE64_STANDARD - .decode(&resp.signature) - .map_err(CloudKmsError::FailedToDecodeSignature) + SignerKey::Ckms(ckms) => { + let mut hasher = Sha256::new(); + hasher.update(message); + let digest = hasher.finalize(); + + let check = crc32c(&digest); + + let response = ckms + .client + .projects() + .locations_key_rings_crypto_keys_crypto_key_versions_asymmetric_sign( + AsymmetricSignRequest { + data: None, + data_crc32c: None, + digest: Some(google_cloudkms1::api::Digest { + sha256: Some(digest.to_vec()), + sha384: None, + sha512: None, + }), + digest_crc32c: Some(check as i64), + }, + &ckms.key_name, + ) + .doit() + .await; + + tracing::info!("Received response from remote signer"); + + let signature = match response { + Ok((_, response)) => { + tracing::info!("Library deserialization succeeded"); + response + .signature + .ok_or_else(|| Box::new(CloudKmsError::MissingSignature)) + } + Err(google_cloudkms1::Error::JsonDecodeError(body, _)) => { + tracing::info!("Using fallback deserialization"); + serde_json::from_str::(&body) + .map_err(CloudKmsError::FailedToDeserialize) .map_err(Box::new) - .and_then(|decoded| { - let check = crc32c(&decoded); - let check_valid = resp - .signature_crc32c - .parse::() - .map(|resp_check| resp_check == check) - .unwrap_or(false); - - if check_valid { - Ok(decoded) - } else { - Err(Box::new(CloudKmsError::CorruptedSignature)) - } + .and_then(|resp| { + BASE64_STANDARD + .decode(&resp.signature) + .map_err(CloudKmsError::FailedToDecodeSignature) + .map_err(Box::new) + .and_then(|decoded| { + let check = crc32c(&decoded); + let check_valid = resp + .signature_crc32c + .parse::() + .map(|resp_check| resp_check == check) + .unwrap_or(false); + + if check_valid { + Ok(decoded) + } else { + Err(Box::new(CloudKmsError::CorruptedSignature)) + } + }) }) - }) + } + Err(err) => Err(Box::new(CloudKmsError::from(err))), + }?; + + Ok(signature) } - Err(err) => Err(Box::new(CloudKmsError::from(err))), - }?; + } + } +} - Ok(signature) +#[async_trait] +impl Sign for Signer { + async fn sign(&self, message: &[u8]) -> Result, SigningKeyError> { + Signer::sign(self, message).await } } -impl Verifier for CloudKmsVerifyingKey { +pub enum Verifier { + Local(LocalVerifyingKey), + Ckms(CloudKmsVerifyingKey), +} + +impl Verifier { fn verify(&self, message: &[u8], signature: &[u8]) -> VerificationResult { - let signature = Signature::try_from(signature); - tracing::trace!("Verifying message"); - match signature { - Ok(signature) => { - let verification_result = self.verifying_key.verify(message, &signature); - match verification_result { - Ok(()) => VerificationResult { - verified: true, - errors: vec![None], + match self { + Verifier::Local(local) => { + let signature = Signature::try_from(signature); + tracing::trace!("Verifying message"); + match signature { + Ok(signature) => { + let verification_result = local.verifying_key.verify(message, &signature); + match verification_result { + Ok(()) => VerificationResult { + verified: true, + errors: vec![None], + }, + Err(err) => VerificationResult { + verified: false, + errors: vec![Some(SigningKeyError::from(err))], + }, + } + } + Err(err) => VerificationResult { + verified: false, + errors: vec![Some(SigningKeyError::from(err))], }, + } + } + Verifier::Ckms(ckms) => { + let signature = Signature::try_from(signature); + tracing::trace!("Verifying message"); + match signature { + Ok(signature) => { + let verification_result = ckms.verifying_key.verify(message, &signature); + match verification_result { + Ok(()) => VerificationResult { + verified: true, + errors: vec![None], + }, + Err(err) => VerificationResult { + verified: false, + errors: vec![Some(SigningKeyError::from(err))], + }, + } + } Err(err) => VerificationResult { verified: false, errors: vec![Some(SigningKeyError::from(err))], }, } } - Err(err) => VerificationResult { - verified: false, - errors: vec![Some(SigningKeyError::from(err))], - }, } } } -impl AsymmetricKey { - fn cloud_kms_key_name(&self) -> Option { - match self { - AsymmetricKey::CkmsSigner { - version, - key, - keyring, - location, - project, - .. - } => Some(format!( - "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}", - project, location, keyring, key, version - )), - AsymmetricKey::CkmsVerifier { - version, - key, - keyring, - location, - project, - .. - } => Some(format!( - "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}", - project, location, keyring, key, version - )), - _ => None, - } - } - - pub async fn private_key(&self) -> Result { - Ok(match self { - AsymmetricKey::LocalSigner { private, .. } => { - RsaPrivateKey::from_pkcs8_pem(private.resolve()?.expose_secret()).unwrap() - } - _ => unimplemented!(), - }) - } - - pub fn public_key(&self) -> Result { - Ok(match self { - AsymmetricKey::LocalVerifier { public, .. } => { - RsaPublicKey::from_public_key_pem(public)? - } - AsymmetricKey::LocalSigner { .. } => Err(SigningKeyError::KeyDoesNotSupportFunction)?, - AsymmetricKey::CkmsVerifier { .. } => { - let public_key = block_on(async { - let kms_client = cloud_kms_client().await?; - - Ok::<_, SigningKeyError>( - kms_client - .projects() - .locations_key_rings_crypto_keys_crypto_key_versions_get_public_key( - &self.cloud_kms_key_name().unwrap(), - ) - .doit() - .await - .map_err(CloudKmsError::from) - .map_err(Box::new)? - .1, - ) - })?; - - let pem = public_key - .pem - .ok_or(CloudKmsError::MissingPem) - .map_err(Box::new)?; - RsaPublicKey::from_public_key_pem(&pem)? - } - AsymmetricKey::CkmsSigner { .. } => Err(SigningKeyError::KeyDoesNotSupportFunction)?, - }) - } - - pub fn as_signer(&self) -> Result, SigningKeyError> { - Ok(match self { - AsymmetricKey::LocalSigner { private, .. } => { - let private_key = - RsaPrivateKey::from_pkcs8_pem(private.resolve()?.expose_secret()).unwrap(); - let signing_key = SigningKey::new(private_key); - - Arc::new(LocalSigningKey { signing_key }) - } - AsymmetricKey::CkmsSigner { .. } => Arc::new(CloudKmsSigningKey { - client: block_on(cloud_kms_client())?, - key_name: self.cloud_kms_key_name().unwrap(), - }), - _ => Err(SigningKeyError::KeyDoesNotSupportFunction)?, - }) - } - - pub fn as_verifier(&self) -> Result, SigningKeyError> { - Ok(match self { - AsymmetricKey::LocalVerifier { public, .. } => { - let verifying_key = VerifyingKey::new(RsaPublicKey::from_public_key_pem(public)?); - - Arc::new(LocalVerifyingKey { verifying_key }) - } - AsymmetricKey::CkmsVerifier { .. } => { - let verifying_key = VerifyingKey::new(self.public_key()?); - Arc::new(CloudKmsVerifyingKey { verifying_key }) - } - _ => Err(SigningKeyError::KeyDoesNotSupportFunction)?, - }) +impl Verify for Verifier { + fn verify(&self, message: &[u8], signature: &[u8]) -> VerificationResult { + Verifier::verify(self, message, signature) } } + +// impl AsymmetricKey { +// fn cloud_kms_key_name(&self) -> Option { +// match self { +// AsymmetricKey::CkmsSigner { +// version, +// key, +// keyring, +// location, +// project, +// .. +// } => Some(format!( +// "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}", +// project, location, keyring, key, version +// )), +// AsymmetricKey::CkmsVerifier { +// version, +// key, +// keyring, +// location, +// project, +// .. +// } => Some(format!( +// "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}", +// project, location, keyring, key, version +// )), +// _ => None, +// } +// } + +// pub async fn private_key(&self) -> Result { +// Ok(match self { +// AsymmetricKey::LocalSigner { private, .. } => { +// RsaPrivateKey::from_pkcs8_pem(private.resolve()?.expose_secret()).unwrap() +// } +// _ => unimplemented!(), +// }) +// } + +// pub fn public_key(&self) -> Result { +// Ok(match self { +// AsymmetricKey::LocalVerifier { public, .. } => { +// RsaPublicKey::from_public_key_pem(public)? +// } +// AsymmetricKey::LocalSigner { .. } => Err(SigningKeyError::KeyDoesNotSupportFunction)?, +// AsymmetricKey::CkmsVerifier { .. } => { +// let public_key = block_on(async { +// let kms_client = cloud_kms_client().await?; + +// Ok::<_, SigningKeyError>( +// kms_client +// .projects() +// .locations_key_rings_crypto_keys_crypto_key_versions_get_public_key( +// &self.cloud_kms_key_name().unwrap(), +// ) +// .doit() +// .await +// .map_err(CloudKmsError::from) +// .map_err(Box::new)? +// .1, +// ) +// })?; + +// let pem = public_key +// .pem +// .ok_or(CloudKmsError::MissingPem) +// .map_err(Box::new)?; +// RsaPublicKey::from_public_key_pem(&pem)? +// } +// AsymmetricKey::CkmsSigner { .. } => Err(SigningKeyError::KeyDoesNotSupportFunction)?, +// }) +// } + +// pub fn as_signer(&self) -> Result, SigningKeyError> { +// Ok(match self { +// AsymmetricKey::LocalSigner { private, .. } => { +// let private_key = +// RsaPrivateKey::from_pkcs8_pem(private.resolve()?.expose_secret()).unwrap(); +// let signing_key = SigningKey::new(private_key); + +// Arc::new(LocalSigningKey { signing_key }) +// } +// AsymmetricKey::CkmsSigner { .. } => Arc::new(CloudKmsSigningKey { +// client: block_on(cloud_kms_client())?, +// key_name: self.cloud_kms_key_name().unwrap(), +// }), +// _ => Err(SigningKeyError::KeyDoesNotSupportFunction)?, +// }) +// } + +// pub fn as_verifier(&self) -> Result, SigningKeyError> { +// Ok(match self { +// AsymmetricKey::LocalVerifier { public, .. } => { +// let verifying_key = VerifyingKey::new(RsaPublicKey::from_public_key_pem(public)?); + +// Arc::new(LocalVerifyingKey { verifying_key }) +// } +// AsymmetricKey::CkmsVerifier { .. } => { +// let verifying_key = VerifyingKey::new(self.public_key()?); +// Arc::new(CloudKmsVerifyingKey { verifying_key }) +// } +// _ => Err(SigningKeyError::KeyDoesNotSupportFunction)?, +// }) +// } +// } diff --git a/v-api/src/config.rs b/v-api/src/config.rs index 1854077..dd032c8 100644 --- a/v-api/src/config.rs +++ b/v-api/src/config.rs @@ -2,6 +2,19 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use futures::executor::block_on; +use jsonwebtoken::jwk::{ + AlgorithmParameters, CommonParameters, Jwk, KeyAlgorithm, PublicKeyUse, RSAKeyParameters, + RSAKeyType, +}; +use rsa::{ + pkcs1v15::{SigningKey, VerifyingKey}, + pkcs8::{DecodePrivateKey, DecodePublicKey}, + traits::PublicKeyParts, + RsaPrivateKey, RsaPublicKey, +}; +use secrecy::ExposeSecret; use serde::{ de::{self, Visitor}, Deserialize, Deserializer, @@ -10,6 +23,14 @@ use std::path::PathBuf; use thiserror::Error; use v_api_param::StringParam; +use crate::{ + authn::{ + jwt::JwtSignerError, CloudKmsError, CloudKmsSigningKey, CloudKmsVerifyingKey, + LocalSigningKey, LocalVerifyingKey, Signer, SignerKey, SigningKeyError, Verifier, + }, + util::cloud_kms_client, +}; + #[derive(Debug, Error)] pub enum AppConfigError { #[error("Encountered invalid log format.")] @@ -70,7 +91,7 @@ impl Default for JwtConfig { pub enum AsymmetricKey { LocalVerifier { kid: String, - public: String, + public: StringParam, }, LocalSigner { kid: String, @@ -149,3 +170,124 @@ pub struct OAuthWebConfig { pub client_secret: StringParam, pub redirect_uri: String, } + +impl AsymmetricKey { + pub fn resolve_signer(&self, path: Option) -> Result { + Ok(Signer::new( + self.kid().to_string(), + match self { + AsymmetricKey::LocalSigner { private, .. } => { + let private_key = + RsaPrivateKey::from_pkcs8_pem(private.resolve(path)?.expose_secret()) + .unwrap(); + let signing_key = SigningKey::new(private_key); + SignerKey::Local(LocalSigningKey::new(signing_key)) + } + AsymmetricKey::CkmsSigner { .. } => SignerKey::Ckms(CloudKmsSigningKey::new( + block_on(cloud_kms_client())?, + self.cloud_kms_key_name().unwrap(), + )), + _ => Err(SigningKeyError::KeyDoesNotSupportFunction)?, + }, + )) + } + + pub async fn resolve_verifier( + &self, + path: Option, + ) -> Result { + Ok(match self { + AsymmetricKey::LocalVerifier { .. } => Verifier::Local(LocalVerifyingKey::new( + VerifyingKey::new(self.public_key(path)?), + )), + AsymmetricKey::CkmsVerifier { .. } => Verifier::Ckms(CloudKmsVerifyingKey::new( + VerifyingKey::new(self.public_key(path)?), + )), + _ => Err(SigningKeyError::KeyDoesNotSupportFunction)?, + }) + } + + pub fn resolve_jwk(&self, path: Option) -> Result { + let key_id = self.kid(); + let public_key = self.public_key(path).map_err(JwtSignerError::InvalidKey)?; + + Ok(Jwk { + common: CommonParameters { + public_key_use: Some(PublicKeyUse::Signature), + key_operations: None, + key_algorithm: Some(KeyAlgorithm::RS256), + key_id: Some(key_id.to_string()), + x509_chain: None, + x509_sha1_fingerprint: None, + x509_sha256_fingerprint: None, + x509_url: None, + }, + algorithm: AlgorithmParameters::RSA(RSAKeyParameters { + key_type: RSAKeyType::RSA, + n: URL_SAFE_NO_PAD.encode(public_key.n().to_bytes_be()), + e: URL_SAFE_NO_PAD.encode(public_key.e().to_bytes_be()), + }), + }) + } + + fn public_key(&self, path: Option) -> Result { + Ok(match self { + AsymmetricKey::LocalVerifier { public, .. } => { + RsaPublicKey::from_public_key_pem(public.resolve(path)?.expose_secret())? + } + AsymmetricKey::CkmsVerifier { .. } => { + let public_key = block_on(async { + let kms_client = cloud_kms_client().await?; + + Ok::<_, SigningKeyError>( + kms_client + .projects() + .locations_key_rings_crypto_keys_crypto_key_versions_get_public_key( + &self.cloud_kms_key_name().unwrap(), + ) + .doit() + .await + .map_err(CloudKmsError::from) + .map_err(Box::new)? + .1, + ) + })?; + + let pem = public_key + .pem + .ok_or(CloudKmsError::MissingPem) + .map_err(Box::new)?; + RsaPublicKey::from_public_key_pem(&pem)? + } + _ => Err(SigningKeyError::KeyDoesNotSupportFunction)?, + }) + } + + fn cloud_kms_key_name(&self) -> Option { + match self { + AsymmetricKey::CkmsSigner { + version, + key, + keyring, + location, + project, + .. + } => Some(format!( + "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}", + project, location, keyring, key, version + )), + AsymmetricKey::CkmsVerifier { + version, + key, + keyring, + location, + project, + .. + } => Some(format!( + "projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/cryptoKeyVersions/{}", + project, location, keyring, key, version + )), + _ => None, + } + } +} diff --git a/v-api/src/context/auth.rs b/v-api/src/context/auth.rs index 238b5ad..5a302d3 100644 --- a/v-api/src/context/auth.rs +++ b/v-api/src/context/auth.rs @@ -11,9 +11,9 @@ use v_model::permissions::Caller; use crate::{ authn::{ jwt::{Claims, JwtSigner, JwtSignerError}, - AuthError, AuthToken, Signer, VerificationResult, Verifier, + AuthError, AuthToken, Sign, Signer, VerificationResult, Verifier, Verify, }, - config::{AsymmetricKey, JwtConfig}, + config::JwtConfig, endpoints::login::oauth::{ OAuthProvider, OAuthProviderError, OAuthProviderFn, OAuthProviderName, }, @@ -34,21 +34,14 @@ impl AuthContext where T: VAppPermission, { - pub fn new(jwt: JwtConfig, keys: Vec) -> Result { - let mut signers = vec![]; - let mut verifiers = vec![]; - - for key in &keys { - match &key { - &AsymmetricKey::LocalSigner { .. } | &AsymmetricKey::CkmsSigner { .. } => { - signers.push(key); - } - &AsymmetricKey::LocalVerifier { .. } | &AsymmetricKey::CkmsVerifier { .. } => { - verifiers.push(key); - } - } - } - + pub fn new( + jwt: JwtConfig, + jwks: JwkSet, + signers: Vec, + verifiers: Vec, + ) -> Result { + let signers = signers.into_iter().map(Arc::new).collect::>(); + let verifiers = verifiers.into_iter().map(Arc::new).collect::>(); Ok(Self { unauthenticated_caller: Caller { id: "00000000-0000-4000-8000-000000000000".parse().unwrap(), @@ -74,31 +67,15 @@ where }, jwt: JwtContext { default_expiration: jwt.default_expiration, - jwks: JwkSet { - keys: verifiers - .iter() - .map(|k| k.as_jwk()) - .collect::, _>>() - .map_err(Box::new)?, - }, + jwks, signers: signers .iter() - .map(|k| JwtSigner::new(k)) - .collect::, _>>() - .map_err(Box::new)?, - }, - secrets: SecretContext { - signers: signers - .iter() - .map(|k| k.as_signer()) - .collect::, _>>() - .map_err(Box::new)?, - verifiers: verifiers - .iter() - .map(|k| k.as_verifier()) + .cloned() + .map(JwtSigner::new) .collect::, _>>() .map_err(Box::new)?, }, + secrets: SecretContext { signers, verifiers }, oauth_providers: HashMap::new(), }) } @@ -137,11 +114,11 @@ where self.jwt.signers.first().unwrap() } - pub fn signer(&self) -> &dyn Signer { + pub fn signer(&self) -> &dyn Sign { &*self.secrets.signers[0] } - pub fn verifiers(&self) -> &[Arc] { + pub fn verifiers(&self) -> &[Arc] { &self.secrets.verifiers } @@ -178,7 +155,7 @@ where } #[async_trait] -impl Verifier for AuthContext +impl Verify for AuthContext where T: VAppPermission, { @@ -201,14 +178,16 @@ pub struct JwtContext { } pub struct SecretContext { - pub signers: Vec>, - pub verifiers: Vec>, + pub signers: Vec>, + pub verifiers: Vec>, } #[cfg(test)] mod tests { + use jsonwebtoken::jwk::JwkSet; + use crate::{ - authn::Verifier, + authn::Verify, config::JwtConfig, context::auth::AuthContext, permissions::VPermission, @@ -226,7 +205,12 @@ mod tests { JwtConfig { default_expiration: 5000, }, - vec![signer, wrong_verifier, verifier], + JwkSet { keys: vec![] }, + vec![signer.resolve_signer(None).unwrap()], + vec![ + wrong_verifier.resolve_verifier(None).await.unwrap(), + verifier.resolve_verifier(None).await.unwrap(), + ], ) .unwrap(); diff --git a/v-api/src/context/link.rs b/v-api/src/context/link.rs index b3a7ceb..c9b2868 100644 --- a/v-api/src/context/link.rs +++ b/v-api/src/context/link.rs @@ -14,7 +14,7 @@ use v_model::{ use crate::{ authn::{ key::{RawKey, SignedKey}, - Signer, + Sign, }, permissions::{VAppPermission, VPermission}, response::{resource_restricted, ResourceResult}, @@ -49,7 +49,7 @@ where pub async fn create_link_request_token( &self, caller: &Caller, - signer: &dyn Signer, + signer: &dyn Sign, source_provider: &TypedUuid, source_user: &TypedUuid, target: &TypedUuid, diff --git a/v-api/src/context/magic_link.rs b/v-api/src/context/magic_link.rs index 82dda71..f0fdbb5 100644 --- a/v-api/src/context/magic_link.rs +++ b/v-api/src/context/magic_link.rs @@ -24,7 +24,7 @@ use v_model::{ use crate::{ authn::{ key::{ApiKeyError, RawKey}, - Signer, SigningKeyError, + Sign, SigningKeyError, }, messenger::{Message, Messenger, MessengerError}, permissions::{VAppPermission, VPermission}, @@ -310,7 +310,7 @@ where pub async fn send_login_attempt( &self, key: RawKey, - signer: &dyn Signer, + signer: &dyn Sign, client_id: TypedUuid, redirect_uri: &Url, medium: MagicLinkMedium, diff --git a/v-api/src/context/mod.rs b/v-api/src/context/mod.rs index 4a775c9..3fb2708 100644 --- a/v-api/src/context/mod.rs +++ b/v-api/src/context/mod.rs @@ -6,9 +6,10 @@ use async_trait::async_trait; use auth::AuthContext; use chrono::{TimeDelta, Utc}; use dropshot::{ClientErrorStatusCode, HttpError, RequestContext, ServerContext}; +use futures::future::join_all; use jsonwebtoken::jwk::JwkSet; use newtype_uuid::TypedUuid; -use std::{fmt::Debug, future::Future, sync::Arc}; +use std::{fmt::Debug, future::Future, path::PathBuf, sync::Arc}; use thiserror::Error; use tracing::instrument; use user::{RegisteredAccessToken, UserContextError}; @@ -27,8 +28,8 @@ use v_model::{ use crate::{ authn::{ - jwt::{Claims, JwtSigner, JwtSignerError}, - AuthError, AuthToken, Signer, VerificationResult, Verifier, + jwt::{Claims, JwtSigner, JwtSignerError, DEFAULT_JWT_EXPIRATION}, + AuthError, AuthToken, Sign, VerificationResult, Verify, }, config::{AsymmetricKey, JwtConfig}, endpoints::login::{ @@ -37,7 +38,7 @@ use crate::{ }, UserInfo, }, - error::{ApiError, AppError}, + error::ApiError, mapper::DefaultMappingEngine, permissions::{VAppPermission, VPermission}, response::{OptionalResource, ResourceErrorInner}, @@ -156,6 +157,12 @@ where ) -> impl Future, Caller), VContextCallerError>>; } +#[derive(Debug, Error)] +pub enum VContextError { + #[error("Failed to construct internal auth context")] + InternalAuthContext, +} + #[derive(Debug, Error)] pub enum VContextCallerError { #[error(transparent)] @@ -215,13 +222,38 @@ impl VContext where T: VAppPermission, { - pub async fn new( + async fn new( public_url: String, + param_path: Option, storage: Arc>, jwt: JwtConfig, keys: Vec, - ) -> Result { - let auth_ctx = AuthContext::new(jwt, keys)?; + ) -> Result { + // `keys` is a list of key components, where each one is either a signer or a verifier. + // Therefore when constructing our lists we omit any keys from each list which is unable + // to be resolved into the needed kind. + let jwks = JwkSet { + keys: keys + .iter() + .filter_map(|key| key.resolve_jwk(param_path.clone()).ok()) + .collect::>(), + }; + let signers = keys + .iter() + .filter_map(|key| key.resolve_signer(param_path.clone()).ok()) + .collect::>(); + let verifiers = join_all( + keys.iter() + .map(|key| key.resolve_verifier(param_path.clone())), + ) + .await + .into_iter() + .filter_map(|key| key.ok()) + .collect::>(); + let auth_ctx = AuthContext::new(jwt, jwks, signers, verifiers).map_err(|err| { + tracing::error!(?err, "Auth context construction failed"); + VContextError::InternalAuthContext + })?; let group_ctx = GroupContext::new(storage.clone()); let mut mapping_ctx = MappingContext::new(storage.clone()); mapping_ctx.set_engine(Some(Arc::new(DefaultMappingEngine::new( @@ -269,7 +301,7 @@ where self.auth.sign_jwt(claims).await } - pub fn signer(&self) -> &dyn Signer { + pub fn signer(&self) -> &dyn Sign { self.auth.signer() } @@ -646,7 +678,7 @@ where } #[async_trait] -impl Verifier for VContext +impl Verify for VContext where T: VAppPermission, { @@ -655,6 +687,100 @@ where } } +#[derive(Debug, Error)] +pub enum VContextBuilderError { + #[error("{0} must be set to build a VContext")] + MissingRequiredConfiguration(String), + #[error("Failed to build VContext")] + VContext(#[from] VContextError), +} + +pub struct VContextBuilder { + param_path: Option, + jwt_expiration: Option, + public_url: Option, + storage: Option>>, + keys: Option>, +} + +impl Default for VContextBuilder +where + T: VAppPermission, +{ + fn default() -> Self { + Self::new() + } +} + +impl VContextBuilder +where + T: VAppPermission, +{ + pub fn new() -> Self { + Self { + param_path: None, + jwt_expiration: None, + public_url: None, + storage: None, + keys: None, + } + } + + pub fn with_param_path(mut self, path: PathBuf) -> Self { + self.param_path = Some(path); + self + } + + pub fn with_jwt_expiration(mut self, expiration: i64) -> Self { + self.jwt_expiration = Some(expiration); + self + } + + pub fn with_public_url(mut self, url: String) -> Self { + self.public_url = Some(url); + self + } + + pub fn with_storage(mut self, storage: Arc>) -> Self { + self.storage = Some(storage); + self + } + + pub fn with_keys(mut self, keys: Vec) -> Self { + self.keys = Some(keys); + self + } + + pub async fn build(self) -> Result, VContextBuilderError> { + let public_url = + self.public_url + .ok_or(VContextBuilderError::MissingRequiredConfiguration( + "public_url".to_string(), + ))?; + let param_path = self.param_path; + let storage = self + .storage + .ok_or(VContextBuilderError::MissingRequiredConfiguration( + "storage".to_string(), + ))?; + let jwt = JwtConfig { + default_expiration: self.jwt_expiration.unwrap_or(DEFAULT_JWT_EXPIRATION), + }; + let keys = self + .keys + .ok_or(VContextBuilderError::MissingRequiredConfiguration( + "keys".to_string(), + ))?; + + VContext::::new(public_url, param_path, storage, jwt, keys) + .await + .map_err(|err| { + tracing::error!(?err, "Failed to construct VContext"); + VContextBuilderError::VContext(err) + }) + } +} + #[cfg(test)] mod tests { use chrono::{TimeDelta, Utc}; @@ -922,6 +1048,7 @@ pub(crate) mod test_mocks { let MockKey { signer, verifier } = mock_key("test"); let mut ctx = VContext::new( "".to_string(), + None, storage, JwtConfig::default(), vec![ diff --git a/v-api/src/context/user.rs b/v-api/src/context/user.rs index 6acb460..5096ad7 100644 --- a/v-api/src/context/user.rs +++ b/v-api/src/context/user.rs @@ -28,7 +28,7 @@ use v_model::{ use crate::{ authn::{ jwt::{Claims, JwtSigner, JwtSignerError}, - AuthToken, Verifier, + AuthToken, Verify, }, permissions::{VAppPermission, VPermission}, response::{ @@ -136,7 +136,7 @@ where token: &AuthToken, ) -> Result, UserContextError> where - U: Verifier, + U: Verify, { let (api_user_id, base_permissions) = self .get_base_permissions(registration_user, verifier, token) @@ -207,7 +207,7 @@ where auth: &AuthToken, ) -> Result<(TypedUuid, BasePermissions), UserContextError> where - U: Verifier, + U: Verify, { match auth { AuthToken::ApiKey(api_key) => { diff --git a/v-api/src/endpoints/login/magic_link/mod.rs b/v-api/src/endpoints/login/magic_link/mod.rs index 7d731e8..2108a5b 100644 --- a/v-api/src/endpoints/login/magic_link/mod.rs +++ b/v-api/src/endpoints/login/magic_link/mod.rs @@ -21,7 +21,7 @@ use v_model::{ }; use crate::{ - authn::{key::RawKey, Verifier}, + authn::{key::RawKey, Verify}, context::magic_link::{MagicLinkSendError, MagicLinkTransitionError}, endpoints::login::{ExternalUserId, UserInfo}, permissions::VAppPermission, @@ -289,14 +289,14 @@ impl From for HttpError { pub trait CheckMagicLinkClient { fn is_secret_valid(&self, key: &RawKey, verifier: &T) -> bool where - T: Verifier; + T: Verify; fn is_redirect_uri_valid(&self, redirect_uri: &str) -> bool; } impl CheckMagicLinkClient for MagicLink { fn is_secret_valid(&self, key: &RawKey, verifier: &T) -> bool where - T: Verifier, + T: Verify, { for secret in &self.secrets { match key.verify(verifier, secret.secret_signature.as_bytes()) { diff --git a/v-api/src/endpoints/login/oauth/mod.rs b/v-api/src/endpoints/login/oauth/mod.rs index 95198ac..f89d822 100644 --- a/v-api/src/endpoints/login/oauth/mod.rs +++ b/v-api/src/endpoints/login/oauth/mod.rs @@ -18,7 +18,7 @@ use thiserror::Error; use tracing::instrument; use v_model::OAuthClient; -use crate::authn::{key::RawKey, Verifier}; +use crate::authn::{key::RawKey, Verify}; use super::{UserInfo, UserInfoError, UserInfoProvider}; @@ -199,16 +199,16 @@ pub struct OAuthProviderNameParam { } pub trait CheckOAuthClient { - fn is_secret_valid(&self, key: &RawKey, verifiers: &T) -> bool + fn is_secret_valid(&self, key: &RawKey, verifier: &T) -> bool where - T: Verifier; + T: Verify; fn is_redirect_uri_valid(&self, redirect_uri: &str) -> bool; } impl CheckOAuthClient for OAuthClient { fn is_secret_valid(&self, key: &RawKey, verifier: &T) -> bool where - T: Verifier, + T: Verify, { for secret in &self.secrets { match key.verify(verifier, secret.secret_signature.as_bytes()) { diff --git a/v-api/src/lib.rs b/v-api/src/lib.rs index 2e7e2f7..1e035d3 100644 --- a/v-api/src/lib.rs +++ b/v-api/src/lib.rs @@ -16,7 +16,8 @@ mod util; pub use context::{ auth::SecretContext, ApiContext, BasePermissions, CallerExtension, ExtensionError, GroupContext, LinkContext, LoginContext, MagicLinkContext, MagicLinkMessage, MagicLinkTarget, - MappingContext, OAuthContext, UserContext, VApiStorage, VContext, VContextWithCaller, + MappingContext, OAuthContext, UserContext, VApiStorage, VContext, VContextBuilder, + VContextBuilderError, VContextError, VContextWithCaller, }; pub use util::response; diff --git a/v-api/src/util.rs b/v-api/src/util.rs index cc8bdb7..4dfcafd 100644 --- a/v-api/src/util.rs +++ b/v-api/src/util.rs @@ -296,7 +296,8 @@ pub mod tests { .as_bytes() .to_vec(), ) - .unwrap(), + .unwrap() + .into(), }, } }