Skip to content

Commit

Permalink
Implement timers to fetch Google certs. (#2774)
Browse files Browse the repository at this point in the history
* Implement timers to fetch Google certs.

* 🤖 cargo-fmt auto-update

* Update did

* 🤖 npm run generate auto-update

* Use http request with closure, move all google related code to google module.

* 🤖 npm run generate auto-update

* Use if else instead of match

* 🤖 cargo-fmt auto-update

* Remove Google specific prefix from state variable scoped to google module.

* Trap if unwrap fails and use match.

---------

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
sea-snake and github-actions[bot] authored Jan 13, 2025
1 parent 19560e5 commit f773987
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 32 deletions.
4 changes: 3 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion src/internet_identity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,9 @@ identity_jose = { git = "https://github.com/dfinity/identity.rs.git", rev = "aa5

# All IC deps
candid.workspace = true
ic-cdk.workspace = true
ic-cdk = { workspace = true, features = ["transform-closure"] }
ic-cdk-macros.workspace = true
ic-cdk-timers.workspace = true
ic-certification.workspace = true
ic-metrics-encoder.workspace = true
ic-stable-structures.workspace = true
Expand Down
1 change: 1 addition & 0 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ fn initialize(maybe_arg: Option<InternetIdentityInit>) {
init_assets(related_origins);
apply_install_arg(maybe_arg);
update_root_hash();
openid::setup_timers();
}

fn apply_install_arg(maybe_arg: Option<InternetIdentityInit>) {
Expand Down
6 changes: 5 additions & 1 deletion src/internet_identity/src/openid.rs
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
pub(crate) mod google;
mod google;

pub fn setup_timers() {
google::setup_timers();
}
81 changes: 52 additions & 29 deletions src/internet_identity/src/openid/google.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,65 @@
use candid::{Deserialize, Nat};
use ic_cdk::api::management_canister::http_request::{
http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs,
TransformContext,
http_request_with_closure, CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse,
};
use ic_cdk::trap;
use ic_cdk::{spawn, trap};
use ic_cdk_timers::set_timer;
use identity_jose::jwk::Jwk;
use serde::Serialize;
use std::cell::RefCell;
use std::cmp::min;
use std::convert::Into;
use std::time::Duration;

const GOOGLE_CERTS_URL: &str = "https://www.googleapis.com/oauth2/v3/certs";
const CERTS_URL: &str = "https://www.googleapis.com/oauth2/v3/certs";

// The amount of cycles needed to make the HTTP outcall with a large enough margin
const CERTS_CALL_CYCLES: u128 = 30_000_000_000;

// The response size is a little under 1KB, so 10KB should give us large enough margin
const MAX_CERTS_RESPONSE_SIZE: u64 = 10_000;

const HTTP_STATUS_OK: u8 = 200;

// Fetch the Google certs every hour, the responses are always
// valid for at least 5 hours so that should be enough margin.
const FETCH_CERTS_INTERVAL: u64 = 60 * 60;

#[derive(Serialize, Deserialize)]
struct GoogleCerts {
keys: Vec<Jwk>,
}

#[allow(unused)]
pub async fn get_certs(transform_method: &str) -> Result<Vec<Jwk>, String> {
thread_local! {
static CERTS: RefCell<Vec<Jwk>> = const { RefCell::new(vec![]) };
}

pub fn setup_timers() {
// Fetch the certs directly after canister initialization.
schedule_fetch_certs(None);
}

fn schedule_fetch_certs(delay: Option<u64>) {
set_timer(Duration::from_secs(delay.unwrap_or(0)), move || {
spawn(async move {
let new_delay = match fetch_certs().await {
Ok(google_certs) => {
CERTS.replace(google_certs);
FETCH_CERTS_INTERVAL
}
// Try again earlier with backoff if fetch failed, the HTTP outcall responses
// aren't the same across nodes when we fetch at the moment of key rotation.
Err(_) => min(FETCH_CERTS_INTERVAL, delay.unwrap_or(60) * 2),
};
schedule_fetch_certs(Some(new_delay));
});
});
}

async fn fetch_certs() -> Result<Vec<Jwk>, String> {
let request = CanisterHttpRequestArgument {
url: GOOGLE_CERTS_URL.into(),
url: CERTS_URL.into(),
method: HttpMethod::GET,
body: None,
max_response_bytes: Some(MAX_CERTS_RESPONSE_SIZE),
transform: Some(TransformContext::from_name(transform_method.into(), vec![])),
max_response_bytes: None,
transform: None,
headers: vec![
HttpHeader {
name: "Accept".into(),
Expand All @@ -43,7 +72,7 @@ pub async fn get_certs(transform_method: &str) -> Result<Vec<Jwk>, String> {
],
};

let (response,) = http_request(request, CERTS_CALL_CYCLES)
let (response,) = http_request_with_closure(request, CERTS_CALL_CYCLES, transform_certs)
.await
.map_err(|(_, err)| err)?;

Expand All @@ -56,17 +85,17 @@ pub async fn get_certs(transform_method: &str) -> Result<Vec<Jwk>, String> {
// so we deserialize, sort the keys and serialize to make the response the same across all nodes.
//
// This function traps since HTTP outcall transforms can't return or log errors anyway.
#[allow(unused)]
pub fn transform_certs(raw: &TransformArgs) -> HttpResponse {
if raw.response.status != HTTP_STATUS_OK {
#[allow(clippy::needless_pass_by_value)]
fn transform_certs(response: HttpResponse) -> HttpResponse {
if response.status != HTTP_STATUS_OK {
trap("Invalid response status")
};

let certs: GoogleCerts = serde_json::from_slice(raw.response.body.as_slice())
.unwrap_or_else(|_| trap("Invalid JSON"));
let certs: GoogleCerts =
serde_json::from_slice(response.body.as_slice()).unwrap_or_else(|_| trap("Invalid JSON"));

let mut sorted_keys = certs.keys.clone();
sorted_keys.sort_by_key(|key| key.kid().unwrap().to_owned());
sorted_keys.sort_by_key(|key| key.kid().unwrap_or_else(|| trap("Invalid JSON")).to_owned());

let body = serde_json::to_vec(&GoogleCerts { keys: sorted_keys })
.unwrap_or_else(|_| trap("Invalid JSON"));
Expand All @@ -82,26 +111,20 @@ pub fn transform_certs(raw: &TransformArgs) -> HttpResponse {
}

#[test]
fn should_transform_to_same() {
fn should_transform_certs_to_same() {
let input = HttpResponse {
status: Nat::from(HTTP_STATUS_OK),
headers: vec![HttpHeader {
name: "Cache-Control".into(),
value: "public, max-age=18544, must-revalidate, no-transform".into()
}],
body: Vec::from(br#"{"keys":[{"e":"AQAB","alg":"RS256","kty":"RSA","kid":"ab8614ff62893badce5aa79a7703b596665d2478","n":"t9OfDNXi2-_bK3_uZizLHS8j8L-Ef4jHjhFvCBbKHkOPOrHQFVoLTSl2e32lIUtxohODogPoYwJKu9uwzpKsMmMj2L2wUwzLB3nxO8M-gOLhIriDWawHMobj3a2ZbVz2eILpjFShU6Ld5f3mQfTV0oHKA_8QnkVfoHsYnexBApJ5xgijiN5BtuK2VPkDLR95XbSnzq604bufWJ3YPSqy8Qc8Y_cFPNtyElePJk9TD2cbnZVpNRUzE7dW9gUtYHFFRrv0jNSKk3XZ-zzkTpz-HqxoNnnyD1c6QK_Ge0tsfsIKdNurRE6Eyuehq9hw-HrI1qdCz-mIqlObQiGdGWx0tQ","use":"sig"},{"use":"sig","alg":"RS256","kty":"RSA","e":"AQAB","n":"wvLUmyAlRhJkFgok97rojtg0xkqsQ6CPPoqRUSXDIYcjfVWMy1Z4hk_-90Y554KTuADfT_0FA46FWb-pr4Scm00gB3CnM8wGLZiaUeDUOu84_Zjh-YPVAua6hz6VFa7cpOUOQ5ZCxCkEQMjtrmei21a6ijy5LS1n9fdiUsjOuYWZSoIQCUj5ow5j2asqYYLRfp0OeymYf6vnttYwz3jS54Xe7tYHW2ZJ_DLCja6mz-9HzIcJH5Tmv5tQRhAUs3aoPKoCQ8ceDHMblDXNV2hBpkv9B6Pk5QVkoDTyEs7lbPagWQ1uz6bdkxM-DnjcMUJ2nh80R_DcbhyqkK4crNrM1w","kid":"89ce3598c473af1bda4bff95e6c8736450206fba"}]}"#)
body: Vec::from(br#"{"keys":[{"e":"AQAB","alg":"RS256","kty":"RSA","kid":"ab8614ff62893badce5aa79a7703b596665d2478","n":"t9OfDNXi2-_bK3_uZizLHS8j8L-Ef4jHjhFvCBbKHkOPOrHQFVoLTSl2e32lIUtxohODogPoYwJKu9uwzpKsMmMj2L2wUwzLB3nxO8M-gOLhIriDWawHMobj3a2ZbVz2eILpjFShU6Ld5f3mQfTV0oHKA_8QnkVfoHsYnexBApJ5xgijiN5BtuK2VPkDLR95XbSnzq604bufWJ3YPSqy8Qc8Y_cFPNtyElePJk9TD2cbnZVpNRUzE7dW9gUtYHFFRrv0jNSKk3XZ-zzkTpz-HqxoNnnyD1c6QK_Ge0tsfsIKdNurRE6Eyuehq9hw-HrI1qdCz-mIqlObQiGdGWx0tQ","use":"sig"},{"use":"sig","alg":"RS256","kty":"RSA","e":"AQAB","n":"wvLUmyAlRhJkFgok97rojtg0xkqsQ6CPPoqRUSXDIYcjfVWMy1Z4hk_-90Y554KTuADfT_0FA46FWb-pr4Scm00gB3CnM8wGLZiaUeDUOu84_Zjh-YPVAua6hz6VFa7cpOUOQ5ZCxCkEQMjtrmei21a6ijy5LS1n9fdiUsjOuYWZSoIQCUj5ow5j2asqYYLRfp0OeymYf6vnttYwz3jS54Xe7tYHW2ZJ_DLCja6mz-9HzIcJH5Tmv5tQRhAUs3aoPKoCQ8ceDHMblDXNV2hBpkv9B6Pk5QVkoDTyEs7lbPagWQ1uz6bdkxM-DnjcMUJ2nh80R_DcbhyqkK4crNrM1w","kid":"89ce3598c473af1bda4bff95e6c8736450206fba"}]}"#),
};
let expected = HttpResponse {
status: Nat::from(HTTP_STATUS_OK),
headers: vec![],
body: Vec::from(br#"{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":"89ce3598c473af1bda4bff95e6c8736450206fba","n":"wvLUmyAlRhJkFgok97rojtg0xkqsQ6CPPoqRUSXDIYcjfVWMy1Z4hk_-90Y554KTuADfT_0FA46FWb-pr4Scm00gB3CnM8wGLZiaUeDUOu84_Zjh-YPVAua6hz6VFa7cpOUOQ5ZCxCkEQMjtrmei21a6ijy5LS1n9fdiUsjOuYWZSoIQCUj5ow5j2asqYYLRfp0OeymYf6vnttYwz3jS54Xe7tYHW2ZJ_DLCja6mz-9HzIcJH5Tmv5tQRhAUs3aoPKoCQ8ceDHMblDXNV2hBpkv9B6Pk5QVkoDTyEs7lbPagWQ1uz6bdkxM-DnjcMUJ2nh80R_DcbhyqkK4crNrM1w","e":"AQAB"},{"kty":"RSA","use":"sig","alg":"RS256","kid":"ab8614ff62893badce5aa79a7703b596665d2478","n":"t9OfDNXi2-_bK3_uZizLHS8j8L-Ef4jHjhFvCBbKHkOPOrHQFVoLTSl2e32lIUtxohODogPoYwJKu9uwzpKsMmMj2L2wUwzLB3nxO8M-gOLhIriDWawHMobj3a2ZbVz2eILpjFShU6Ld5f3mQfTV0oHKA_8QnkVfoHsYnexBApJ5xgijiN5BtuK2VPkDLR95XbSnzq604bufWJ3YPSqy8Qc8Y_cFPNtyElePJk9TD2cbnZVpNRUzE7dW9gUtYHFFRrv0jNSKk3XZ-zzkTpz-HqxoNnnyD1c6QK_Ge0tsfsIKdNurRE6Eyuehq9hw-HrI1qdCz-mIqlObQiGdGWx0tQ","e":"AQAB"}]}"#)
body: Vec::from(br#"{"keys":[{"kty":"RSA","use":"sig","alg":"RS256","kid":"89ce3598c473af1bda4bff95e6c8736450206fba","n":"wvLUmyAlRhJkFgok97rojtg0xkqsQ6CPPoqRUSXDIYcjfVWMy1Z4hk_-90Y554KTuADfT_0FA46FWb-pr4Scm00gB3CnM8wGLZiaUeDUOu84_Zjh-YPVAua6hz6VFa7cpOUOQ5ZCxCkEQMjtrmei21a6ijy5LS1n9fdiUsjOuYWZSoIQCUj5ow5j2asqYYLRfp0OeymYf6vnttYwz3jS54Xe7tYHW2ZJ_DLCja6mz-9HzIcJH5Tmv5tQRhAUs3aoPKoCQ8ceDHMblDXNV2hBpkv9B6Pk5QVkoDTyEs7lbPagWQ1uz6bdkxM-DnjcMUJ2nh80R_DcbhyqkK4crNrM1w","e":"AQAB"},{"kty":"RSA","use":"sig","alg":"RS256","kid":"ab8614ff62893badce5aa79a7703b596665d2478","n":"t9OfDNXi2-_bK3_uZizLHS8j8L-Ef4jHjhFvCBbKHkOPOrHQFVoLTSl2e32lIUtxohODogPoYwJKu9uwzpKsMmMj2L2wUwzLB3nxO8M-gOLhIriDWawHMobj3a2ZbVz2eILpjFShU6Ld5f3mQfTV0oHKA_8QnkVfoHsYnexBApJ5xgijiN5BtuK2VPkDLR95XbSnzq604bufWJ3YPSqy8Qc8Y_cFPNtyElePJk9TD2cbnZVpNRUzE7dW9gUtYHFFRrv0jNSKk3XZ-zzkTpz-HqxoNnnyD1c6QK_Ge0tsfsIKdNurRE6Eyuehq9hw-HrI1qdCz-mIqlObQiGdGWx0tQ","e":"AQAB"}]}"#),
};

assert_eq!(
transform_certs(&TransformArgs {
response: input,
context: vec![]
}),
expected
);
assert_eq!(transform_certs(input), expected);
}

0 comments on commit f773987

Please sign in to comment.