diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 64b92a6..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.gitignore b/.gitignore index aa2ec5d..71e4408 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ target Cargo.lock +.DS_Store # Intellij project files *.iml diff --git a/Cargo.toml b/Cargo.toml index 3360303..6dfb112 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "gcp_auth" -version = "0.6.2" +version = "0.7.0" repository = "https://github.com/hrvolapeter/gcp_auth" description = "Google cloud platform (GCP) authentication using default and custom service accounts" documentation = "https://docs.rs/gcp_auth/" @@ -15,21 +15,23 @@ default = ["hyper-rustls/rustls-native-certs"] webpki-roots = ["hyper-rustls/webpki-roots"] [dependencies] +async-trait = "0.1" base64 = "0.13" -time = { version = "0.3.5", features = ["serde"] } +dirs-next = "2.0" hyper = { version = "0.14.2", features = ["client", "runtime", "http2"] } hyper-rustls = { version = "0.23.0", default-features = false, features = ["native-tokio", "http1", "http2"] } -log = "0.4" +ring = "0.16.20" rustls = "0.20.2" rustls-pemfile = "0.2.1" serde = {version = "1.0", features = ["derive", "rc"]} serde_json = "1.0" +thiserror = "1.0" +time = { version = "0.3.5", features = ["serde"] } tokio = { version = "1.1", features = ["fs", "sync"] } +tracing = "0.1.29" +tracing-futures = "0.2.5" url = "2" which = "4.2" -async-trait = "0.1" -thiserror = "1.0" -dirs-next = "2.0" [dev-dependencies] env_logger = "0.9" diff --git a/examples/simple.rs b/examples/simple.rs index 98af699..be7991f 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,7 +1,7 @@ #[tokio::main] async fn main() -> Result<(), Box> { env_logger::init(); - let authentication_manager = gcp_auth::init().await?; + let authentication_manager = gcp_auth::AuthenticationManager::new().await?; let _token = authentication_manager .get_token(&["https://www.googleapis.com/auth/cloud-platform"]) .await?; diff --git a/src/authentication_manager.rs b/src/authentication_manager.rs index d352e53..647d55d 100644 --- a/src/authentication_manager.rs +++ b/src/authentication_manager.rs @@ -1,8 +1,12 @@ use async_trait::async_trait; use tokio::sync::Mutex; +use crate::custom_service_account::CustomServiceAccount; +use crate::default_authorized_user::DefaultAuthorizedUser; +use crate::default_service_account::DefaultServiceAccount; use crate::error::Error; -use crate::types::{HyperClient, Token}; +use crate::gcloud_authorized_user::GCloudAuthorizedUser; +use crate::types::{self, HyperClient, Token}; #[async_trait] pub(crate) trait ServiceAccount: Send + Sync { @@ -13,7 +17,9 @@ pub(crate) trait ServiceAccount: Send + Sync { /// Authentication manager is responsible for caching and obtaing credentials for the required scope /// -/// Cacheing for the full life time is ensured +/// Construct the authentication manager with [`AuthenticationManager::new()`] or by creating +/// a [`CustomServiceAccount`], then converting it into an `AuthenticationManager` using the `From` +/// impl. pub struct AuthenticationManager { pub(crate) client: HyperClient, pub(crate) service_account: Box, @@ -21,10 +27,61 @@ pub struct AuthenticationManager { } impl AuthenticationManager { - pub(crate) fn new(client: HyperClient, service_account: Box) -> Self { + /// Finds a service account provider to get authentication tokens from + /// + /// Tries the following approaches, in order: + /// + /// 1. Check if the `GOOGLE_APPLICATION_CREDENTIALS` environment variable if set; + /// if so, use a custom service account as the token source. + /// 2. Check if the `gcloud` tool is available on the `PATH`; if so, use the + /// `gcloud auth print-access-token` command as the token source. + /// 3. Send a HTTP request to the internal metadata server to retrieve a token; + /// if it succeeds, use the default service account as the token source. + /// 4. Look for credentials in `.config/gcloud/application_default_credentials.json`; + /// if found, use these credentials to request refresh tokens. + #[tracing::instrument] + pub async fn new() -> Result { + tracing::debug!("Initializing gcp_auth"); + if let Some(service_account) = CustomServiceAccount::from_env()? { + return Ok(service_account.into()); + } + + let client = types::client(); + let gcloud_error = match GCloudAuthorizedUser::new() { + Ok(service_account) => { + tracing::debug!("Using GCloudAuthorizedUser"); + return Ok(Self::build(client, service_account)); + } + Err(e) => e, + }; + + let default_service_error = match DefaultServiceAccount::new(&client).await { + Ok(service_account) => { + tracing::debug!("Using DefaultServiceAccount"); + return Ok(Self::build(client, service_account)); + } + Err(e) => e, + }; + + let default_user_error = match DefaultAuthorizedUser::new(&client).await { + Ok(service_account) => { + tracing::debug!("Using DefaultAuthorizedUser"); + return Ok(Self::build(client, service_account)); + } + Err(e) => e, + }; + + Err(Error::NoAuthMethod( + Box::new(gcloud_error), + Box::new(default_service_error), + Box::new(default_user_error), + )) + } + + fn build(client: HyperClient, service_account: impl ServiceAccount + 'static) -> Self { Self { client, - service_account, + service_account: Box::new(service_account), refresh_mutex: Mutex::new(()), } } @@ -58,3 +115,9 @@ impl AuthenticationManager { self.service_account.project_id(&self.client).await } } + +impl From for AuthenticationManager { + fn from(service_account: CustomServiceAccount) -> Self { + Self::build(types::client(), service_account) + } +} diff --git a/src/custom_service_account.rs b/src/custom_service_account.rs index 36df77a..0b8d22b 100644 --- a/src/custom_service_account.rs +++ b/src/custom_service_account.rs @@ -1,36 +1,78 @@ use std::collections::HashMap; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::sync::RwLock; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use tokio::fs; use crate::authentication_manager::ServiceAccount; use crate::error::Error; -use crate::types::{HyperClient, Token}; +use crate::types::{HyperClient, Signer, Token}; use crate::util::HyperExt; +/// A custom service account containing credentials +/// +/// Once initialized, a [`CustomServiceAccount`] can be converted into an [`AuthenticationManager`] +/// using the applicable `From` implementation. #[derive(Debug)] -pub(crate) struct CustomServiceAccount { - tokens: RwLock, Token>>, +pub struct CustomServiceAccount { credentials: ApplicationCredentials, + signer: Signer, + tokens: RwLock, Token>>, } impl CustomServiceAccount { - pub(crate) async fn from_file(path: &Path) -> Result { - Ok(Self { - credentials: ApplicationCredentials::from_file(path).await?, - tokens: RwLock::new(HashMap::new()), - }) + /// Check `GOOGLE_APPLICATION_CREDENTIALS` environment variable for a path to JSON credentials + pub fn from_env() -> Result, Error> { + std::env::var_os("GOOGLE_APPLICATION_CREDENTIALS") + .map(|path| { + tracing::debug!( + "Reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var" + ); + Self::from_file(&PathBuf::from(path)) + }) + .transpose() + } + + /// Read service account credentials from the given JSON file + pub fn from_file>(path: T) -> Result { + let file = std::fs::File::open(path.as_ref()).map_err(Error::CustomServiceAccountPath)?; + match serde_json::from_reader::<_, ApplicationCredentials>(file) { + Ok(credentials) => Self::new(credentials), + Err(e) => Err(Error::CustomServiceAccountCredentials(e)), + } + } + + /// Read service account credentials from the given JSON string + pub fn from_json(s: &str) -> Result { + match serde_json::from_str::(s) { + Ok(credentials) => Self::new(credentials), + Err(e) => Err(Error::CustomServiceAccountCredentials(e)), + } } - pub(crate) fn from_json(s: &str) -> Result { + fn new(credentials: ApplicationCredentials) -> Result { Ok(Self { - credentials: ApplicationCredentials::from_json(s)?, + signer: Signer::new(&credentials.private_key)?, + credentials, tokens: RwLock::new(HashMap::new()), }) } + + /// The RSA PKCS1 SHA256 [`Signer`] used to sign JWT tokens + pub fn signer(&self) -> &Signer { + &self.signer + } + + /// The project ID as found in the credentials + pub fn project_id(&self) -> Option<&str> { + self.credentials.project_id.as_deref() + } + + /// The private key as found in the credentials + pub fn private_key_pem(&self) -> &str { + &self.credentials.private_key + } } #[async_trait] @@ -47,31 +89,31 @@ impl ServiceAccount for CustomServiceAccount { self.tokens.read().unwrap().get(&key).cloned() } + #[tracing::instrument] async fn refresh_token(&self, client: &HyperClient, scopes: &[&str]) -> Result { use crate::jwt::Claims; - use crate::jwt::JwtSigner; use crate::jwt::GRANT_TYPE; use hyper::header; use url::form_urlencoded; - let signer = JwtSigner::new(&self.credentials.private_key)?; - - let claims = Claims::new(&self.credentials, scopes, None); - let signed = signer.sign_claims(&claims).map_err(Error::TLSError)?; + let jwt = Claims::new(&self.credentials, scopes, None).to_jwt(&self.signer)?; let rqbody = form_urlencoded::Serializer::new(String::new()) - .extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", signed.as_str())]) + .extend_pairs(&[("grant_type", GRANT_TYPE), ("assertion", jwt.as_str())]) .finish(); + let request = hyper::Request::post(&self.credentials.token_uri) .header(header::CONTENT_TYPE, "application/x-www-form-urlencoded") .body(hyper::Body::from(rqbody)) .unwrap(); - log::debug!("requesting token from service account: {:?}", request); + + tracing::debug!("requesting token from service account: {:?}", request); let token = client .request(request) .await .map_err(Error::OAuthConnectionError)? .deserialize::() .await?; + let key = scopes.iter().map(|x| (*x).to_string()).collect(); self.tokens.write().unwrap().insert(key, token.clone()); Ok(token) @@ -100,16 +142,3 @@ pub(crate) struct ApplicationCredentials { /// client_x509_cert_url pub(crate) client_x509_cert_url: Option, } - -impl ApplicationCredentials { - async fn from_file>(path: T) -> Result { - let content = fs::read_to_string(path) - .await - .map_err(Error::ApplicationProfilePath)?; - ApplicationCredentials::from_json(&content) - } - - fn from_json(s: &str) -> Result { - serde_json::from_str(s).map_err(Error::ApplicationProfileFormat) - } -} diff --git a/src/default_authorized_user.rs b/src/default_authorized_user.rs index 51f916b..a27e81d 100644 --- a/src/default_authorized_user.rs +++ b/src/default_authorized_user.rs @@ -1,11 +1,10 @@ -use std::path::Path; +use std::fs; use std::sync::RwLock; use async_trait::async_trait; use hyper::body::Body; use hyper::{Method, Request}; use serde::{Deserialize, Serialize}; -use tokio::fs; use crate::authentication_manager::ServiceAccount; use crate::error::Error; @@ -36,17 +35,23 @@ impl DefaultAuthorizedUser { .unwrap() } + #[tracing::instrument] async fn get_token(client: &HyperClient) -> Result { - log::debug!("Loading user credentials file"); + tracing::debug!("Loading user credentials file"); let mut home = dirs_next::home_dir().ok_or(Error::NoHomeDir)?; home.push(Self::USER_CREDENTIALS_PATH); - let cred = UserCredentials::from_file(home.display().to_string()).await?; + + let file = fs::File::open(home).map_err(Error::UserProfilePath)?; + let cred = serde_json::from_reader::<_, UserCredentials>(file) + .map_err(Error::UserProfileFormat)?; + let req = Self::build_token_request(&RefreshRequest { client_id: cred.client_id, client_secret: cred.client_secret, grant_type: "refresh_token".to_string(), refresh_token: cred.refresh_token, }); + let token = client .request(req) .await @@ -93,12 +98,3 @@ struct UserCredentials { /// Type pub(crate) r#type: String, } - -impl UserCredentials { - async fn from_file>(path: T) -> Result { - let content = fs::read_to_string(path) - .await - .map_err(Error::UserProfilePath)?; - Ok(serde_json::from_str(&content).map_err(Error::UserProfileFormat)?) - } -} diff --git a/src/default_service_account.rs b/src/default_service_account.rs index 863a9f4..6d782e5 100644 --- a/src/default_service_account.rs +++ b/src/default_service_account.rs @@ -34,8 +34,9 @@ impl DefaultServiceAccount { .unwrap() } + #[tracing::instrument] async fn get_token(client: &HyperClient) -> Result { - log::debug!("Getting token from GCP instance metadata server"); + tracing::debug!("Getting token from GCP instance metadata server"); let req = Self::build_token_request(Self::DEFAULT_TOKEN_GCP_URI); let token = client .request(req) @@ -50,7 +51,7 @@ impl DefaultServiceAccount { #[async_trait] impl ServiceAccount for DefaultServiceAccount { async fn project_id(&self, client: &HyperClient) -> Result { - log::debug!("Getting project ID from GCP instance metadata server"); + tracing::debug!("Getting project ID from GCP instance metadata server"); let req = Self::build_token_request(Self::DEFAULT_PROJECT_ID_GCP_URI); let rsp = client.request(req).await.map_err(Error::ConnectionError)?; diff --git a/src/error.rs b/src/error.rs index 12f2b02..64d7b0f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -17,35 +17,25 @@ pub enum Error { /// Error in underlying RustTLS library. /// Might signal problem with establishing secure connection using trusted certificates #[error("TLS error")] - TLSError(#[source] rustls::Error), + TlsError(#[source] rustls::Error), /// Error when establishing connection to OAuth server #[error("Could not establish connection with OAuth server")] OAuthConnectionError(#[source] hyper::Error), - /// Error when parsing response from OAuth server - #[error("Could not parse OAuth server response")] - OAuthParsingError(#[source] serde_json::error::Error), - - /// Variable `GOOGLE_APPLICATION_CREDENTIALS` could not be found in the current environment - /// - /// GOOGLE_APPLICATION_CREDENTIALS is used for providing path to json file with applications credentials. - /// File can be downloaded in GCP Console when creating service account. - #[error("Path to custom auth credentials was not provided in `GOOGLE_APPLICATION_CREDENTIALS` env variable")] - ApplicationProfileMissing, - - /// Wrong path to custom application profile credentials provided + /// Wrong path to custom service account credentials provided /// - /// Path has to be defined using `GOOGLE_APPLICATION_CREDENTIALS` environment variable - #[error("Environment variable `GOOGLE_APPLICATION_CREDENTIALS` contains invalid path to application profile file")] - ApplicationProfilePath(#[source] std::io::Error), + /// By default, the custom service account credentials are parsed from the path pointed to by the + /// `GOOGLE_APPLICATION_CREDENTIALS` environment variable. + #[error("Invalid path to custom service account")] + CustomServiceAccountPath(#[source] std::io::Error), - /// Wrong format of custom application profile + /// Failed to parse the application credentials provided /// - /// Application profile is downloaded from GCP console and is stored in filesystem on the server. - /// Full path is passed to library by setting `GOOGLE_APPLICATION_CREDENTIALS` variable with path as a value. + /// By default, the custom service account credentials are parsed from the path pointed to by the + /// `GOOGLE_APPLICATION_CREDENTIALS` environment variable. #[error("Application profile provided in `GOOGLE_APPLICATION_CREDENTIALS` was not parsable")] - ApplicationProfileFormat(#[source] serde_json::error::Error), + CustomServiceAccountCredentials(#[source] serde_json::error::Error), /// Default user profile not found /// @@ -70,9 +60,9 @@ pub enum Error { #[error("Server unavailable: {0}")] ServerUnavailable(String), - /// Could not determine signer scheme - #[error("Couldn't choose signing scheme")] - SignerSchemeError, + /// Could not sign requested message + #[error("Could not sign")] + SignerFailed, /// Could not initialize signer #[error("Couldn't initialize signer")] diff --git a/src/gcloud_authorized_user.rs b/src/gcloud_authorized_user.rs index f82dfd6..2c6fee1 100644 --- a/src/gcloud_authorized_user.rs +++ b/src/gcloud_authorized_user.rs @@ -1,8 +1,6 @@ use crate::authentication_manager::ServiceAccount; use crate::error::Error; -use crate::error::Error::{ - GCloudError, GCloudNotFound, GCloudParseError, NoProjectId, ParsingError, -}; +use crate::error::Error::{GCloudError, GCloudNotFound, GCloudParseError, ParsingError}; use crate::types::HyperClient; use crate::Token; use async_trait::async_trait; @@ -17,7 +15,7 @@ pub(crate) struct GCloudAuthorizedUser { } impl GCloudAuthorizedUser { - pub(crate) async fn new() -> Result { + pub(crate) fn new() -> Result { which("gcloud") .map_err(|_| GCloudNotFound) .map(|path| Self { gcloud: path }) @@ -27,7 +25,15 @@ impl GCloudAuthorizedUser { #[async_trait] impl ServiceAccount for GCloudAuthorizedUser { async fn project_id(&self, _: &HyperClient) -> Result { - Err(NoProjectId) + let mut command = Command::new(&self.gcloud); + command.args(&["config", "get-value", "project"]); + + match command.output() { + Ok(output) if output.status.success() => { + String::from_utf8(output.stdout).map_err(|_| GCloudParseError) + } + _ => Err(Error::ProjectIdNotFound), + } } fn get_token(&self, _scopes: &[&str]) -> Option { @@ -38,14 +44,13 @@ impl ServiceAccount for GCloudAuthorizedUser { let mut command = Command::new(&self.gcloud); command.args(&["auth", "print-access-token", "--quiet"]); - match command.output() { - Ok(output) if output.status.success() => String::from_utf8(output.stdout) - .map_err(|_| GCloudParseError) - .and_then(|access_token| { - serde_json::from_value::(json!({ "access_token": access_token.trim() })) - .map_err(ParsingError) - }), - _ => Err(GCloudError), - } + let output = match command.output() { + Ok(output) if output.status.success() => output.stdout, + _ => return Err(GCloudError), + }; + + let access_token = String::from_utf8(output).map_err(|_| GCloudParseError)?; + serde_json::from_value::(json!({ "access_token": access_token.trim() })) + .map_err(ParsingError) } } diff --git a/src/jwt.rs b/src/jwt.rs index bb02572..e11a280 100644 --- a/src/jwt.rs +++ b/src/jwt.rs @@ -1,16 +1,10 @@ //! Copyright (c) 2016 Google Inc (lewinb@google.com). -use std::io; - -use rustls::{ - self, - sign::{self, SigningKey}, - PrivateKey, -}; use serde::Serialize; use crate::custom_service_account::ApplicationCredentials; use crate::error::Error; +use crate::types::Signer; pub(crate) const GRANT_TYPE: &str = "urn:ietf:params:oauth:grant-type:jwt-bearer"; const GOOGLE_RS256_HEAD: &str = r#"{"alg":"RS256","typ":"JWT"}"#; @@ -20,26 +14,6 @@ fn append_base64 + ?Sized>(s: &T, out: &mut String) { base64::encode_config_buf(s, base64::URL_SAFE, out) } -/// Decode a PKCS8 formatted RSA key. -fn decode_rsa_key(pem_pkcs8: &str) -> Result { - let private_keys = rustls_pemfile::pkcs8_private_keys(&mut pem_pkcs8.as_bytes()); - - match private_keys { - Ok(mut keys) if !keys.is_empty() => { - keys.truncate(1); - Ok(PrivateKey(keys.remove(0))) - } - Ok(_) => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Not enough private keys in PEM", - )), - Err(_) => Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Error reading key from PEM", - )), - } -} - /// Permissions requested for a JWT. /// See https://developers.google.com/identity/protocols/OAuth2ServiceAccount#authorizingrequests. #[derive(Serialize, Debug)] @@ -78,38 +52,16 @@ impl<'a> Claims<'a> { scope, } } -} - -/// A JSON Web Token ready for signing. -pub(crate) struct JwtSigner { - signer: Box, -} -impl JwtSigner { - pub(crate) fn new(private_key: &str) -> Result { - let key = decode_rsa_key(private_key)?; - let signing_key = sign::RsaSigningKey::new(&key).map_err(|_| Error::SignerInit)?; - let signer = signing_key - .choose_scheme(&[rustls::SignatureScheme::RSA_PKCS1_SHA256]) - .ok_or(Error::SignerSchemeError)?; - Ok(JwtSigner { signer }) - } - - pub(crate) fn sign_claims(&self, claims: &Claims) -> Result { - let mut jwt_head = Self::encode_claims(claims); - let signature = self.signer.sign(jwt_head.as_bytes())?; - jwt_head.push('.'); - append_base64(&signature, &mut jwt_head); - Ok(jwt_head) - } + pub(crate) fn to_jwt(&self, signer: &Signer) -> Result { + let mut jwt = String::new(); + append_base64(GOOGLE_RS256_HEAD, &mut jwt); + jwt.push('.'); + append_base64(&serde_json::to_string(self).unwrap(), &mut jwt); - /// Encodes the first two parts (header and claims) to base64 and assembles them into a form - /// ready to be signed. - fn encode_claims(claims: &Claims) -> String { - let mut head = String::new(); - append_base64(GOOGLE_RS256_HEAD, &mut head); - head.push('.'); - append_base64(&serde_json::to_string(&claims).unwrap(), &mut head); - head + let signature = signer.sign(jwt.as_bytes())?; + jwt.push('.'); + append_base64(&signature, &mut jwt); + Ok(jwt) } } diff --git a/src/lib.rs b/src/lib.rs index 86c86b3..0e73757 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,98 +71,7 @@ mod jwt; mod types; mod util; -use custom_service_account::CustomServiceAccount; - pub use authentication_manager::AuthenticationManager; +pub use custom_service_account::CustomServiceAccount; pub use error::Error; -pub use types::Token; - -use std::path::Path; - -use hyper::Client; -use hyper_rustls::HttpsConnectorBuilder; - -/// Initialize GCP authentication based on a credentials file path -/// -/// Returns `AuthenticationManager` which can be used to obtain tokens -pub async fn from_credentials_file>( - path: T, -) -> Result { - let custom = CustomServiceAccount::from_file(path.as_ref()).await?; - get_authentication_manager(Some(custom)).await -} - -/// Initialize GCP authentication based on a JSON string -/// -/// Returns `AuthenticationManager` which can be used to obtain tokens -pub async fn from_credentials_json(s: &str) -> Result { - let custom = CustomServiceAccount::from_json(s)?; - get_authentication_manager(Some(custom)).await -} - -async fn get_authentication_manager( - custom: Option, -) -> Result { - #[cfg(feature = "webpki-roots")] - let https = HttpsConnectorBuilder::new().with_webpki_roots(); - #[cfg(not(feature = "webpki-roots"))] - let https = HttpsConnectorBuilder::new().with_native_roots(); - - let client = - Client::builder().build::<_, hyper::Body>(https.https_or_http().enable_http2().build()); - - if let Some(service_account) = custom { - log::debug!("Using CustomServiceAccount"); - return Ok(AuthenticationManager::new( - client, - Box::new(service_account), - )); - } - let gcloud = gcloud_authorized_user::GCloudAuthorizedUser::new().await; - if let Ok(service_account) = gcloud { - log::debug!("Using GCloudAuthorizedUser"); - return Ok(AuthenticationManager::new( - client.clone(), - Box::new(service_account), - )); - } - let default = default_service_account::DefaultServiceAccount::new(&client).await; - if let Ok(service_account) = default { - log::debug!("Using DefaultServiceAccount"); - return Ok(AuthenticationManager::new( - client.clone(), - Box::new(service_account), - )); - } - let user = default_authorized_user::DefaultAuthorizedUser::new(&client).await; - if let Ok(user_account) = user { - log::debug!("Using DefaultAuthorizedUser"); - return Ok(AuthenticationManager::new(client, Box::new(user_account))); - } - Err(Error::NoAuthMethod( - Box::new(gcloud.unwrap_err()), - Box::new(default.unwrap_err()), - Box::new(user.unwrap_err()), - )) -} -/// Initialize GCP authentication -/// -/// Returns `AuthenticationManager` which can be used to obtain tokens -pub async fn init() -> Result { - log::debug!("Initializing gcp_auth"); - - // will return an error if the environment variable isn’t set, in which case custom is set to - // none. - let custom = match std::env::var("GOOGLE_APPLICATION_CREDENTIALS") { - Ok(path) => { - log::debug!("Reading credentials file from GOOGLE_APPLICATION_CREDENTIALS env var"); - - // We know that GOOGLE_APPLICATION_CREDENTIALS exists, read the file and return an - // error in case of failure. - Some(CustomServiceAccount::from_file(Path::new(&path)).await?) - } - Err(_) => None, - }; - - get_authentication_manager(custom).await -} +pub use types::{Signer, Token}; diff --git a/src/types.rs b/src/types.rs index 0789bac..6e93b58 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,10 +1,18 @@ -use std::fmt; use std::sync::Arc; - +use std::{fmt, io}; + +use hyper::Client; +use hyper_rustls::HttpsConnectorBuilder; +use ring::{ + rand::SystemRandom, + signature::{RsaKeyPair, RSA_PKCS1_SHA256}, +}; use serde::Deserializer; use serde::{Deserialize, Serialize}; use time::{Duration, OffsetDateTime}; +use crate::Error; + /// Represents an access token. All access tokens are Bearer tokens. /// /// Tokens should not be cached, the [`AuthenticationManager`] handles the correct caching @@ -68,6 +76,59 @@ impl Token { } } +/// An RSA PKCS1 SHA256 signer +pub struct Signer { + key: RsaKeyPair, + rng: SystemRandom, +} + +impl Signer { + pub(crate) fn new(pem_pkcs8: &str) -> Result { + let private_keys = rustls_pemfile::pkcs8_private_keys(&mut pem_pkcs8.as_bytes()); + + let key = match private_keys { + Ok(mut keys) if !keys.is_empty() => { + keys.truncate(1); + keys.remove(0) + } + Ok(_) => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Not enough private keys in PEM", + ) + .into()) + } + Err(_) => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Error reading key from PEM", + ) + .into()) + } + }; + + Ok(Signer { + key: RsaKeyPair::from_pkcs8(&key).map_err(|_| Error::SignerInit)?, + rng: SystemRandom::new(), + }) + } + + /// Sign the input message and return the signature + pub fn sign(&self, input: &[u8]) -> Result, Error> { + let mut signature = vec![0; self.key.public_modulus_len()]; + self.key + .sign(&RSA_PKCS1_SHA256, &self.rng, input, &mut signature) + .map_err(|_| Error::SignerFailed)?; + Ok(signature) + } +} + +impl fmt::Debug for Signer { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Signer").finish() + } +} + fn deserialize_time<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -78,6 +139,15 @@ where Ok(s) } +pub(crate) fn client() -> HyperClient { + #[cfg(feature = "webpki-roots")] + let https = HttpsConnectorBuilder::new().with_webpki_roots(); + #[cfg(not(feature = "webpki-roots"))] + let https = HttpsConnectorBuilder::new().with_native_roots(); + + Client::builder().build::<_, hyper::Body>(https.https_or_http().enable_http2().build()) +} + pub(crate) type HyperClient = hyper::Client>; diff --git a/src/util.rs b/src/util.rs index a2940f6..68e6a25 100644 --- a/src/util.rs +++ b/src/util.rs @@ -27,7 +27,7 @@ impl HyperExt for hyper::Response { parts.status, String::from_utf8_lossy(body.as_ref()) ); - log::error!("{}", error); + tracing::error!("{}", error); return Err(Error::ServerUnavailable(error)); }