diff --git a/Cargo.lock b/Cargo.lock index ecaa51d7a..0e4350045 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,7 +548,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4114279215a005bc675e386011e594e1d9b800918cea18fcadadcce864a2046b" dependencies = [ "borsh-derive 0.10.3", - "hashbrown 0.12.3", + "hashbrown 0.11.2", ] [[package]] @@ -1468,9 +1468,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.16" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" +checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" dependencies = [ "bytes", "fnv", @@ -1499,9 +1499,6 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash", -] [[package]] name = "heck" @@ -2005,6 +2002,7 @@ dependencies = [ "tower-http 0.4.0", "tracing", "tracing-subscriber", + "yup-oauth2", ] [[package]] @@ -2599,6 +2597,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] + [[package]] name = "oauth2" version = "4.3.0" @@ -3484,6 +3491,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "secp256k1" version = "0.24.3" @@ -3948,6 +3961,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" dependencies = [ "itoa", + "libc", + "num_threads", "serde", "time-core", "time-macros", @@ -4895,6 +4910,33 @@ dependencies = [ "libc", ] +[[package]] +name = "yup-oauth2" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6135ad28f1957d676384074df0ad1dd920966834bddd973da86119378b4964d7" +dependencies = [ + "anyhow", + "async-trait", + "base64 0.13.1", + "futures", + "http", + "hyper", + "hyper-rustls 0.24.0", + "itertools", + "log", + "percent-encoding", + "rustls 0.21.0", + "rustls-pemfile", + "seahash", + "serde", + "serde_json", + "time 0.3.20", + "tokio", + "tower-service", + "url", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/mpc-recovery/Cargo.toml b/mpc-recovery/Cargo.toml index 7aa199db2..b865f391c 100644 --- a/mpc-recovery/Cargo.toml +++ b/mpc-recovery/Cargo.toml @@ -48,3 +48,4 @@ near-jsonrpc-primitives = "0.16.1" near-primitives = "0.16.1" near-crypto = "0.16.1" tower-http = { version = "0.4.0", features = ["cors"] } +yup-oauth2 = "8" diff --git a/mpc-recovery/src/gcp/auth.rs b/mpc-recovery/src/gcp/auth.rs deleted file mode 100644 index 8f6381fc9..000000000 --- a/mpc-recovery/src/gcp/auth.rs +++ /dev/null @@ -1,122 +0,0 @@ -// This file is a slightly modified version of -// https://raw.githubusercontent.com/google-apis-rs/google-cloud-rs/e3569046fd571469f058e045bb6c3a8ba241fd73/google-cloud/src/authorize/mod.rs -use chrono::offset::Utc; -use chrono::DateTime; -use hyper::client::{Client, HttpConnector}; -use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder}; -use serde::{Deserialize, Serialize}; -use std::fmt; - -#[allow(unused)] -pub(crate) const TLS_CERTS: &[u8] = include_bytes!("../../roots.pem"); - -const AUTH_ENDPOINT: &str = "https://oauth2.googleapis.com/token"; - -/// Represents application credentials for accessing Google Cloud Platform services. -#[allow(missing_docs)] -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -pub struct ApplicationCredentials { - #[serde(rename = "type")] - pub cred_type: String, - pub project_id: String, - pub private_key_id: String, - pub private_key: String, - pub client_email: String, - pub client_id: String, - pub auth_uri: String, - pub token_uri: String, - pub auth_provider_x509_cert_url: String, - pub client_x509_cert_url: String, -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum TokenValue { - Bearer(String), -} - -impl fmt::Display for TokenValue { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - TokenValue::Bearer(token) => write!(f, "Bearer {}", token.as_str()), - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct Token { - value: TokenValue, - expiry: DateTime, -} - -#[derive(Debug, Clone)] -pub(crate) struct TokenManager { - client: Client>, - scopes: String, - creds: ApplicationCredentials, - current_token: Option, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -struct AuthResponse { - access_token: String, -} - -impl TokenManager { - pub(crate) fn new(creds: ApplicationCredentials, scopes: &[&str]) -> TokenManager { - let connector = HttpsConnectorBuilder::new() - .with_native_roots() - .https_only() - .enable_all_versions() - .build(); - TokenManager { - creds, - client: Client::builder().build::<_, hyper::Body>(connector), - scopes: scopes.join(" "), - current_token: None, - } - } - - pub(crate) async fn token(&mut self) -> anyhow::Result { - let hour = chrono::Duration::minutes(45); - let current_time = chrono::Utc::now(); - match self.current_token { - Some(ref token) if token.expiry >= current_time => Ok(token.value.to_string()), - _ => { - let expiry = current_time + hour; - let claims = serde_json::json!({ - "iss": self.creds.client_email.as_str(), - "scope": self.scopes.as_str(), - "aud": AUTH_ENDPOINT, - "exp": expiry.timestamp(), - "iat": current_time.timestamp(), - }); - let token = jsonwebtoken::encode( - &jsonwebtoken::Header::new(jsonwebtoken::Algorithm::RS256), - &claims, - &jsonwebtoken::EncodingKey::from_rsa_pem(self.creds.private_key.as_bytes())?, - )?; - let form = format!( - "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion={}", - token.as_str() - ); - - let req = hyper::Request::builder() - .method("POST") - .uri(AUTH_ENDPOINT) - .header("Content-Type", "application/x-www-form-urlencoded") - .body(hyper::Body::from(form))?; - let data = hyper::body::to_bytes(self.client.request(req).await?.into_body()) - .await? - .to_vec(); - - let ar: AuthResponse = serde_json::from_slice(&data)?; - - let value = TokenValue::Bearer(ar.access_token); - let token = value.to_string(); - self.current_token = Some(Token { expiry, value }); - - Ok(token) - } - } - } -} diff --git a/mpc-recovery/src/gcp/mod.rs b/mpc-recovery/src/gcp/mod.rs index 4cc12e13c..f0d4fcaee 100644 --- a/mpc-recovery/src/gcp/mod.rs +++ b/mpc-recovery/src/gcp/mod.rs @@ -1,55 +1,69 @@ -use std::fs::File; - +use hyper::client::HttpConnector; +use hyper_rustls::HttpsConnector; use mpc_recovery_gcp::google::cloud::secretmanager::v1::{ secret_manager_service_client::SecretManagerServiceClient, AccessSecretVersionRequest, }; use tonic::{ transport::{Certificate, Channel, ClientTlsConfig}, - Request, + Request, Status, +}; +use yup_oauth2::{ + authenticator::{ApplicationDefaultCredentialsTypes, Authenticator}, + ApplicationDefaultCredentialsAuthenticator, ApplicationDefaultCredentialsFlowOpts, }; - -use self::auth::TokenManager; - -mod auth; const DOMAIN_NAME: &str = "secretmanager.googleapis.com"; const ENDPOINT: &str = "https://secretmanager.googleapis.com"; -const SCOPES: [&str; 1] = ["https://www.googleapis.com/auth/cloud-platform"]; const TLS_CERTS: &[u8] = include_bytes!("../../roots.pem"); -pub async fn load_secret_share(node_id: u64) -> anyhow::Result> { - // GOOGLE_APPLICATION_CREDENTIALS points to the credentials file on GCP: - // https://cloud.google.com/docs/authentication/application-default-credentials - let path = std::env::var("GOOGLE_APPLICATION_CREDENTIALS")?; - let file = File::open(path)?; - let creds = serde_json::from_reader(file)?; - let mut token_manager = TokenManager::new(creds, &SCOPES); - let token = token_manager.token().await?; - - let tls_config = ClientTlsConfig::new() - .ca_certificate(Certificate::from_pem(TLS_CERTS)) - .domain_name(DOMAIN_NAME); - - let channel = Channel::from_static(ENDPOINT) - .tls_config(tls_config)? - .connect() - .await?; - let mut client = - SecretManagerServiceClient::with_interceptor(channel, move |mut req: Request<()>| { - req.metadata_mut() - .insert("authorization", token.parse().unwrap()); - Ok(req) - }); - let request = Request::new(AccessSecretVersionRequest { - name: format!( - "projects/pagoda-discovery-platform-dev/secrets/mpc-recovery-secret-share-{node_id}/versions/latest" - ), - }); - - let response = client.access_secret_version(request).await?; - let secret_payload = response - .into_inner() - .payload - .ok_or_else(|| anyhow::anyhow!("failed to fetch secret share from GCP Secret Manager"))?; - Ok(secret_payload.data) +pub struct GcpService { + authenticator: Authenticator>, +} + +impl GcpService { + pub async fn new() -> anyhow::Result { + let opts = ApplicationDefaultCredentialsFlowOpts::default(); + let authenticator = match ApplicationDefaultCredentialsAuthenticator::builder(opts).await { + ApplicationDefaultCredentialsTypes::InstanceMetadata(auth) => auth.build().await?, + ApplicationDefaultCredentialsTypes::ServiceAccount(auth) => auth.build().await?, + }; + + Ok(Self { authenticator }) + } + + pub async fn load_secret(&self, name: String) -> anyhow::Result> { + let access_token = self + .authenticator + .token(&["https://www.googleapis.com/auth/cloud-platform"]) + .await?; + let token = access_token + .token() + .ok_or_else(|| anyhow::anyhow!("GCP token did not have access_token field in it"))?; + + let tls_config = ClientTlsConfig::new() + .ca_certificate(Certificate::from_pem(TLS_CERTS)) + .domain_name(DOMAIN_NAME); + + let channel = Channel::from_static(ENDPOINT) + .tls_config(tls_config)? + .connect() + .await?; + let mut client = + SecretManagerServiceClient::with_interceptor(channel, move |mut req: Request<()>| { + req.metadata_mut().insert( + "authorization", + format!("Bearer {}", token) + .parse() + .map_err(|_| Status::unauthenticated("failed to parse access token"))?, + ); + Ok(req) + }); + + let request = Request::new(AccessSecretVersionRequest { name }); + let response = client.access_secret_version(request).await?; + let secret_payload = response.into_inner().payload.ok_or_else(|| { + anyhow::anyhow!("failed to fetch secret share from GCP Secret Manager") + })?; + Ok(secret_payload.data) + } } diff --git a/mpc-recovery/src/main.rs b/mpc-recovery/src/main.rs index 2356c7d3b..76287d69a 100644 --- a/mpc-recovery/src/main.rs +++ b/mpc-recovery/src/main.rs @@ -1,4 +1,5 @@ use clap::Parser; +use gcp::GcpService; use mpc_recovery::LeaderConfig; use near_primitives::types::AccountId; use threshold_crypto::{serde_impl::SerdeSecret, PublicKeySet, SecretKeyShare}; @@ -49,7 +50,7 @@ enum Cli { account_creator_id: AccountId, /// TEMPORARY - Account creator ed25519 secret key #[arg(long, env("MPC_RECOVERY_ACCOUNT_CREATOR_SK"))] - account_creator_sk: String, + account_creator_sk: Option, }, StartSign { /// Node ID @@ -67,10 +68,35 @@ enum Cli { }, } -async fn load_sh_skare(node_id: u64, sk_share_arg: Option) -> anyhow::Result { +async fn load_sh_skare( + gcp_service: &GcpService, + node_id: u64, + sk_share_arg: Option, +) -> anyhow::Result { match sk_share_arg { Some(sk_share) => Ok(sk_share), - None => Ok(std::str::from_utf8(&gcp::load_secret_share(node_id).await?)?.to_string()), + None => { + let name = format!( + "projects/pagoda-discovery-platform-dev/secrets/mpc-recovery-secret-share-{node_id}/versions/latest" + ); + Ok(std::str::from_utf8(&gcp_service.load_secret(name).await?)?.to_string()) + } + } +} + +async fn load_account_creator_sk( + gcp_service: &GcpService, + node_id: u64, + account_creator_sk_arg: Option, +) -> anyhow::Result { + match account_creator_sk_arg { + Some(account_creator_sk) => Ok(account_creator_sk), + None => { + let name = format!( + "projects/pagoda-discovery-platform-dev/secrets/mpc-recovery-account-creator-sk-{node_id}/versions/latest" + ); + Ok(std::str::from_utf8(&gcp_service.load_secret(name).await?)?.to_string()) + } } } @@ -104,10 +130,14 @@ async fn main() -> anyhow::Result<()> { account_creator_id, account_creator_sk, } => { - let sk_share = load_sh_skare(node_id, sk_share).await?; + let gcp_service = GcpService::new().await?; + let sk_share = load_sh_skare(&gcp_service, node_id, sk_share).await?; + let account_creator_sk = + load_account_creator_sk(&gcp_service, node_id, account_creator_sk).await?; - let pk_set: PublicKeySet = serde_json::from_str(&pk_set).unwrap(); - let sk_share: SecretKeyShare = serde_json::from_str(&sk_share).unwrap(); + let pk_set: PublicKeySet = serde_json::from_str(&pk_set)?; + let sk_share: SecretKeyShare = serde_json::from_str(&sk_share)?; + let account_creator_sk = account_creator_sk.parse()?; mpc_recovery::run_leader_node(LeaderConfig { id: node_id, @@ -120,8 +150,7 @@ async fn main() -> anyhow::Result<()> { near_root_account, // TODO: Create such an account for testnet and mainnet in a secure way account_creator_id, - // TODO: Load this account secret key from GCP Secret Manager - account_creator_sk: account_creator_sk.parse()?, + account_creator_sk, }) .await; } @@ -131,7 +160,8 @@ async fn main() -> anyhow::Result<()> { sk_share, web_port, } => { - let sk_share = load_sh_skare(node_id, sk_share).await?; + let gcp_service = GcpService::new().await?; + let sk_share = load_sh_skare(&gcp_service, node_id, sk_share).await?; let pk_set: PublicKeySet = serde_json::from_str(&pk_set).unwrap(); let sk_share: SecretKeyShare = serde_json::from_str(&sk_share).unwrap();