Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add jwt_signature_pk_urls to state #866

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions integration-tests/fastauth/src/env/containers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@ impl SignerNode<'_> {
cipher_key: Some(hex::encode(cipher_key)),
gcp_project_id: ctx.gcp_project_id.clone(),
gcp_datastore_url: Some(ctx.datastore.address.clone()),
jwt_signature_pk_url: ctx.oidc_provider.jwt_pk_url.clone(),
jwt_signature_pk_urls: vec![ctx.oidc_provider.jwt_pk_url.clone()],
logging_options: logging::Options::default(),
}
.into_str_args();
Expand Down Expand Up @@ -667,7 +667,7 @@ impl<'a> LeaderNode<'a> {
fast_auth_partners_filepath: None,
gcp_project_id: ctx.gcp_project_id.clone(),
gcp_datastore_url: Some(ctx.datastore.address.to_string()),
jwt_signature_pk_url: ctx.oidc_provider.jwt_pk_url.to_string(),
jwt_signature_pk_urls: vec![ctx.oidc_provider.jwt_pk_url.clone()],
logging_options: logging::Options::default(),
}
.into_str_args();
Expand Down
4 changes: 2 additions & 2 deletions integration-tests/fastauth/src/env/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ impl SignerNode {
cipher_key: Some(hex::encode(cipher_key)),
gcp_project_id: ctx.gcp_project_id.clone(),
gcp_datastore_url: Some(ctx.datastore.local_address.clone()),
jwt_signature_pk_url: ctx.oidc_provider.jwt_pk_local_url.clone(),
jwt_signature_pk_urls: vec![ctx.oidc_provider.jwt_pk_local_url.clone()],
logging_options: logging::Options::default(),
};

Expand Down Expand Up @@ -118,7 +118,7 @@ impl LeaderNode {
),
gcp_project_id: ctx.gcp_project_id.clone(),
gcp_datastore_url: Some(ctx.datastore.local_address.clone()),
jwt_signature_pk_url: ctx.oidc_provider.jwt_pk_local_url.clone(),
jwt_signature_pk_urls: vec![ctx.oidc_provider.jwt_pk_local_url.clone()],
logging_options: logging::Options::default(),
};

Expand Down
14 changes: 7 additions & 7 deletions mpc-recovery/src/leader_node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ pub struct Config {
// TODO: temporary solution
pub account_creator_signer: KeyRotatingSigner,
pub partners: PartnerList,
pub jwt_signature_pk_url: String,
pub jwt_signature_pk_urls: Vec<String>,
}

pub async fn run(config: Config) {
Expand All @@ -59,7 +59,7 @@ pub async fn run(config: Config) {
near_root_account,
account_creator_signer,
partners,
jwt_signature_pk_url,
jwt_signature_pk_urls
} = config;
let _span = tracing::debug_span!("run", env, port);
tracing::debug!(?sign_nodes, "running a leader node");
Expand All @@ -74,7 +74,7 @@ pub async fn run(config: Config) {
near_root_account: near_root_account.parse().unwrap(),
account_creator_signer,
partners,
jwt_signature_pk_url,
jwt_signature_pk_urls
});

// Get keys from all sign nodes, and broadcast them out as a set.
Expand Down Expand Up @@ -198,7 +198,7 @@ struct LeaderState {
// TODO: temporary solution
account_creator_signer: KeyRotatingSigner,
partners: PartnerList,
jwt_signature_pk_url: String,
jwt_signature_pk_urls: Vec<String>
}

async fn mpc_public_key(
Expand Down Expand Up @@ -302,7 +302,7 @@ async fn process_user_credentials(
&request.oidc_token,
Some(&state.partners.oidc_providers()),
&state.reqwest_client,
&state.jwt_signature_pk_url,
&state.jwt_signature_pk_urls,
)
.await
.map_err(LeaderNodeError::OidcVerificationFailed)?;
Expand Down Expand Up @@ -334,7 +334,7 @@ async fn process_new_account(
&request.oidc_token,
Some(&state.partners.oidc_providers()),
&state.reqwest_client,
&state.jwt_signature_pk_url,
&state.jwt_signature_pk_urls,
)
.await
.map_err(LeaderNodeError::OidcVerificationFailed)?;
Expand Down Expand Up @@ -477,7 +477,7 @@ async fn process_sign(
&request.oidc_token,
Some(&state.partners.oidc_providers()),
&state.reqwest_client,
&state.jwt_signature_pk_url,
&state.jwt_signature_pk_urls,
)
.await
.map_err(LeaderNodeError::OidcVerificationFailed)?;
Expand Down
36 changes: 20 additions & 16 deletions mpc-recovery/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,9 +113,9 @@ pub enum Cli {
/// GCP datastore URL
#[arg(long, env("MPC_RECOVERY_GCP_DATASTORE_URL"))]
gcp_datastore_url: Option<String>,
/// URL to the public key used to sign JWT tokens
#[arg(long, env("MPC_RECOVERY_JWT_SIGNATURE_PK_URL"))]
jwt_signature_pk_url: String,
/// URLs of the public keys used by all issuers
#[arg(long, value_parser, num_args = 1.., value_delimiter = ',', env("MPC_RECOVERY_JWT_SIGNATURE_PK_URLS"))]
jwt_signature_pk_urls: Vec<String>,
/// Enables export of span data using opentelemetry protocol.
#[clap(flatten)]
logging_options: logging::Options,
Expand All @@ -142,9 +142,9 @@ pub enum Cli {
/// GCP datastore URL
#[arg(long, env("MPC_RECOVERY_GCP_DATASTORE_URL"))]
gcp_datastore_url: Option<String>,
/// URL to the public key used to sign JWT tokens
#[arg(long, env("MPC_RECOVERY_JWT_SIGNATURE_PK_URL"))]
jwt_signature_pk_url: String,
/// URLs of the public keys used by all issuers
#[arg(long, value_parser, num_args = 1.., value_delimiter = ',', env("MPC_RECOVERY_JWT_SIGNATURE_PK_URLS"))]
jwt_signature_pk_urls: Vec<String>,
/// Enables export of span data using opentelemetry protocol.
#[clap(flatten)]
logging_options: logging::Options,
Expand Down Expand Up @@ -203,7 +203,7 @@ pub async fn run(cmd: Cli) -> anyhow::Result<()> {
fast_auth_partners_filepath: partners_filepath,
gcp_project_id,
gcp_datastore_url,
jwt_signature_pk_url,
jwt_signature_pk_urls,
logging_options,
} => {
let _subscriber_guard = logging::subscribe_global(
Expand Down Expand Up @@ -231,7 +231,7 @@ pub async fn run(cmd: Cli) -> anyhow::Result<()> {
near_root_account,
account_creator_signer,
partners,
jwt_signature_pk_url,
jwt_signature_pk_urls
};

run_leader_node(config).await;
Expand All @@ -244,7 +244,7 @@ pub async fn run(cmd: Cli) -> anyhow::Result<()> {
web_port,
gcp_project_id,
gcp_datastore_url,
jwt_signature_pk_url,
jwt_signature_pk_urls,
logging_options,
} => {
let _subscriber_guard = logging::subscribe_global(
Expand Down Expand Up @@ -272,7 +272,7 @@ pub async fn run(cmd: Cli) -> anyhow::Result<()> {
node_key: sk_share,
cipher,
port: web_port,
jwt_signature_pk_url,
jwt_signature_pk_urls
};
run_sign_node(config).await;
}
Expand Down Expand Up @@ -428,7 +428,7 @@ impl Cli {
fast_auth_partners_filepath,
gcp_project_id,
gcp_datastore_url,
jwt_signature_pk_url,
jwt_signature_pk_urls,
logging_options,
} => {
let mut buf = vec![
Expand All @@ -445,8 +445,6 @@ impl Cli {
account_creator_id.to_string(),
"--gcp-project-id".to_string(),
gcp_project_id,
"--jwt-signature-pk-url".to_string(),
jwt_signature_pk_url,
];

if let Some(partners) = fast_auth_partners {
Expand All @@ -465,6 +463,10 @@ impl Cli {
buf.push("--sign-nodes".to_string());
buf.push(sign_node);
}
for url in jwt_signature_pk_urls {
buf.push("--jwt-signature-pk-urls".to_string());
buf.push(url);
}
let account_creator_sk = serde_json::to_string(&account_creator_sk).unwrap();
buf.push("--account-creator-sk".to_string());
buf.push(account_creator_sk);
Expand All @@ -480,7 +482,7 @@ impl Cli {
sk_share,
gcp_project_id,
gcp_datastore_url,
jwt_signature_pk_url,
jwt_signature_pk_urls,
logging_options,
} => {
let mut buf = vec![
Expand All @@ -493,8 +495,6 @@ impl Cli {
web_port.to_string(),
"--gcp-project-id".to_string(),
gcp_project_id,
"--jwt-signature-pk-url".to_string(),
jwt_signature_pk_url,
];
if let Some(key) = cipher_key {
buf.push("--cipher-key".to_string());
Expand All @@ -508,6 +508,10 @@ impl Cli {
buf.push("--gcp-datastore-url".to_string());
buf.push(gcp_datastore_url);
}
for url in jwt_signature_pk_urls {
buf.push("--jwt-signature-pk-urls".to_string());
buf.push(url);
}
buf.extend(logging_options.into_str_args());

buf
Expand Down
89 changes: 75 additions & 14 deletions mpc-recovery/src/oauth.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use anyhow::{Context, Result};
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use jsonwebtoken::{Algorithm, DecodingKey};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use serde_json::Value;

use crate::firewall::allowed::OidcProviderList;
use crate::primitives::InternalAccountId;
Expand All @@ -13,12 +15,12 @@ pub async fn verify_oidc_token(
token: &OidcToken,
oidc_providers: Option<&OidcProviderList>,
client: &reqwest::Client,
jwt_signature_pk_url: &str,
jwt_signature_pk_urls: &[String],
) -> anyhow::Result<IdTokenClaims> {
let public_keys = get_pagoda_firebase_public_keys(client, jwt_signature_pk_url)
let public_keys = get_public_keys(client, jwt_signature_pk_urls)
.await
.map_err(|e| anyhow::anyhow!("failed to get Firebase public key: {e}"))?;
tracing::info!("verify_oidc_token firebase public keys: {public_keys:?}");
.map_err(|e| anyhow::anyhow!("failed to get public keys: {e}"))?;
tracing::info!("verify_oidc_token public keys: {public_keys:?}");

let mut last_occured_error =
anyhow::anyhow!("Unexpected error. Firebase public keys not found");
Expand Down Expand Up @@ -99,13 +101,69 @@ impl IdTokenClaims {
}
}

pub async fn get_pagoda_firebase_public_keys(
pub async fn get_public_keys(
client: &reqwest::Client,
jwt_signature_pk_url: &str,
) -> anyhow::Result<Vec<String>> {
let response = client.get(jwt_signature_pk_url).send().await?;
let json: HashMap<String, String> = response.json().await?;
Ok(json.into_values().collect())
jwt_signature_pk_urls: &[String],
) -> Result<Vec<String>> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to create a map of keys: "provider" -> "[keys]".
Otherwise, somebody will create a token using provider1, but verify it with key 2.

let mut all_keys = Vec::new();

for url in jwt_signature_pk_urls {
match fetch_and_parse_keys(client, url).await {
Ok(mut keys) => all_keys.append(&mut keys),
Err(e) => tracing::warn!("Failed to fetch keys from {}: {}", url, e),
}
}

if all_keys.is_empty() {
anyhow::bail!("No valid public keys found from any source");
}

Ok(all_keys)
}

async fn fetch_and_parse_keys(client: &reqwest::Client, url: &str) -> Result<Vec<String>> {
let response = client
.get(url)
.send()
.await
.context("Failed to send request")?;

let json: Value = response.json().await.context("Failed to parse JSON")?;

match json {
Value::Object(obj) if obj.contains_key("keys") => parse_jwks_format(&obj),
Value::Object(obj) => parse_firebase_format(&obj),
_ => {
tracing::warn!("Unexpected response format from {}", url);
Ok(vec![])
}
}
}

fn parse_jwks_format(obj: &serde_json::Map<String, Value>) -> Result<Vec<String>> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's test this

obj["keys"]
.as_array()
.context("'keys' is not an array")?
.iter()
.filter_map(|key| match (key["n"].as_str(), key["e"].as_str()) {
(Some(n), Some(e)) => Some(format_rsa_key(n, e)),
_ => None,
})
.collect::<Result<Vec<_>>>()
}

fn parse_firebase_format(obj: &serde_json::Map<String, Value>) -> Result<Vec<String>> {
Ok(obj
.values()
.filter_map(|value| value.as_str().map(String::from))
.collect())
}

fn format_rsa_key(n: &str, e: &str) -> Result<String> {
Ok(format!(
"-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----",
BASE64.encode(format!("{}:{}", n, e))
))
}

#[cfg(test)]
Expand All @@ -121,10 +179,13 @@ mod tests {

#[tokio::test]
async fn test_get_pagoda_firebase_public_key() {
let url =
"https://www.googleapis.com/robot/v1/metadata/x509/[email protected]";
let urls = vec![
"https://www.googleapis.com/robot/v1/metadata/x509/[email protected]".to_string(),
"https://www.googleapis.com/oauth2/v3/certs".to_string(),
];
let client = reqwest::Client::new();
let pk = get_pagoda_firebase_public_keys(&client, url).await.unwrap();
let pk = get_public_keys(&client, &urls).await.unwrap();

assert!(!pk.is_empty());
}

Expand Down
14 changes: 7 additions & 7 deletions mpc-recovery/src/sign_node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub struct Config {
pub node_key: ExpandedKeyPair,
pub cipher: Aes256Gcm,
pub port: u16,
pub jwt_signature_pk_url: String,
pub jwt_signature_pk_urls: Vec<String>,
}

pub async fn run(config: Config) {
Expand All @@ -50,7 +50,7 @@ pub async fn run(config: Config) {
node_key,
cipher,
port,
jwt_signature_pk_url,
jwt_signature_pk_urls
} = config;
let our_index = usize::try_from(our_index).expect("This index is way to big");

Expand All @@ -66,7 +66,7 @@ pub async fn run(config: Config) {
cipher,
signing_state: SigningState::new(),
node_info: NodeInfo::new(our_index, pk_set.map(|set| set.public_keys)),
jwt_signature_pk_url,
jwt_signature_pk_urls
});

let app = Router::new()
Expand Down Expand Up @@ -101,7 +101,7 @@ struct SignNodeState {
cipher: Aes256Gcm,
signing_state: SigningState,
node_info: NodeInfo,
jwt_signature_pk_url: String,
jwt_signature_pk_urls: Vec<String>,
}

async fn get_or_generate_user_creds(
Expand Down Expand Up @@ -213,7 +213,7 @@ async fn process_commit(
&request.oidc_token,
None,
&state.reqwest_client,
&state.jwt_signature_pk_url,
&state.jwt_signature_pk_urls,
)
.await
.map_err(SignNodeError::OidcVerificationFailed)?;
Expand Down Expand Up @@ -369,9 +369,9 @@ async fn process_public_key(
&request.oidc_token,
None,
&state.reqwest_client,
&state.jwt_signature_pk_url,
&state.jwt_signature_pk_urls,
)
.await
.await
.map_err(SignNodeError::OidcVerificationFailed)?;

let frp_pk = request.frp_public_key;
Expand Down
Loading