Skip to content

Commit

Permalink
Add well-known asset for Related Origin Requests (#2720)
Browse files Browse the repository at this point in the history
* Add well-known asset for Related Origin Requests

* ROR from config

* Uncomment code

* Fix rebase issues

* Add testing

* Fix formatting

* Improve comments
  • Loading branch information
lmuntaner authored Dec 6, 2024
1 parent e186505 commit 4118aae
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 4 deletions.
6 changes: 6 additions & 0 deletions src/asset_util/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ lazy_static! {
let mut map = HashMap::new();
map.insert(Path::new(".well-known/ic-domains").to_owned(), (ContentType::JSON, ContentEncoding::Identity));
map.insert(Path::new(".well-known/ii-alternative-origins").to_owned(), (ContentType::JSON, ContentEncoding::Identity));
map.insert(Path::new(".well-known/webauthn").to_owned(), (ContentType::JSON, ContentEncoding::Identity));
map
};
}
Expand Down Expand Up @@ -645,6 +646,11 @@ fn should_return_correct_extension() {
ContentType::JSON,
ContentEncoding::Identity,
),
(
".well-known/webauthn",
ContentType::JSON,
ContentEncoding::Identity,
),
(
".well-known/ii-alternative-origins",
ContentType::JSON,
Expand Down
23 changes: 20 additions & 3 deletions src/internet_identity/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use ic_cdk::api;
use include_dir::{include_dir, Dir};
use serde_json::json;
use sha2::Digest;

// used both in init and post_upgrade
pub fn init_assets() {
pub fn init_assets(maybe_related_origins: Option<Vec<String>>) {
state::assets_mut(|certified_assets| {
let assets = get_static_assets();
let assets = get_static_assets(maybe_related_origins);

// Extract integrity hashes for all inlined scripts, from all the HTML files.
let integrity_hashes = assets
Expand Down Expand Up @@ -47,7 +48,7 @@ fn fixup_html(html: &str) -> String {
static ASSET_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../../dist");

// Gets the static assets. All static assets are prepared only once (like injecting the canister ID).
pub fn get_static_assets() -> Vec<Asset> {
pub fn get_static_assets(maybe_related_origins: Option<Vec<String>>) -> Vec<Asset> {
let mut assets = collect_assets(&ASSET_DIR, Some(fixup_html));

// Required to make II available on the identity.internetcomputer.org domain.
Expand All @@ -58,6 +59,22 @@ pub fn get_static_assets() -> Vec<Asset> {
encoding: ContentEncoding::Identity,
content_type: ContentType::OCTETSTREAM,
});

if let Some(related_origins) = maybe_related_origins {
// Required to share passkeys with the different domains. Maximum of 5 labels.
// See https://web.dev/articles/webauthn-related-origin-requests#step_2_set_up_your_well-knownwebauthn_json_file_in_site-1
let content = json!({
"origins": related_origins,
})
.to_string()
.into_bytes();
assets.push(Asset {
url_path: "/.well-known/webauthn".to_string(),
content,
encoding: ContentEncoding::Identity,
content_type: ContentType::JSON,
});
}
assets
}

Expand Down
7 changes: 6 additions & 1 deletion src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,12 @@ fn post_upgrade(maybe_arg: Option<InternetIdentityInit>) {
}

fn initialize(maybe_arg: Option<InternetIdentityInit>) {
init_assets();
let state_related_origins = state::persistent_state(|storage| storage.related_origins.clone());
let related_origins = maybe_arg
.clone()
.map(|arg| arg.related_origins)
.unwrap_or(state_related_origins);
init_assets(related_origins);
apply_install_arg(maybe_arg);
update_root_hash();
}
Expand Down
129 changes: 129 additions & 0 deletions src/internet_identity/tests/integration/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use internet_identity_interface::internet_identity::types::{
};
use pocket_ic::{CallError, PocketIc};
use serde_bytes::ByteBuf;
use serde_json::json;
use std::collections::HashMap;
use std::io::Read;
use std::time::Duration;
Expand Down Expand Up @@ -76,6 +77,134 @@ fn ii_canister_serves_http_assets() -> Result<(), CallError> {
Ok(())
}

/// Verifies that `.well-known/webauthn` assets are delivered, certified and have security headers if present in the config.
#[test]
fn ii_canister_serves_webauthn_assets() -> Result<(), CallError> {
let env = env();
let related_origins: Vec<String> = [
"https://identity.internetcomputer.org".to_string(),
"https://identity.ic0.app".to_string(),
]
.to_vec();
let config = InternetIdentityInit {
assigned_user_number_range: None,
archive_config: None,
canister_creation_cycles_cost: None,
register_rate_limit: None,
captcha_config: None,
related_origins: Some(related_origins.clone()),
};
let canister_id = install_ii_canister_with_arg(&env, II_WASM.clone(), Some(config));

for certification_version in 1..=2 {
let request = HttpRequest {
method: "GET".to_string(),
url: "/.well-known/webauthn".to_string(),
headers: vec![],
body: ByteBuf::new(),
certificate_version: Some(certification_version),
};
let http_response = http_request(&env, canister_id, &request)?;
let response_body = String::from_utf8_lossy(&http_response.body).to_string();

assert_eq!(http_response.status_code, 200);

let expected_content = json!({
"origins": related_origins,
})
.to_string();
assert_eq!(response_body, expected_content);

// check the appropriate Content-Type header is set
let (_, content_type) = http_response
.headers
.iter()
.find(|(name, _)| name.to_lowercase() == "content-type")
.expect("Content-Encoding header not found");
assert_eq!(
content_type, "application/json",
"unexpected Content-Encoding header value"
);
verify_security_headers(&http_response.headers);

let result = verify_response_certification(
&env,
canister_id,
request,
http_response,
certification_version,
);
assert_eq!(result.verification_version, certification_version);
}
Ok(())
}

#[test]
fn ii_canister_serves_webauthn_assets_after_upgrade() -> Result<(), CallError> {
let env = env();
let related_origins: Vec<String> = [
"https://identity.internetcomputer.org".to_string(),
"https://identity.ic0.app".to_string(),
]
.to_vec();
let config = InternetIdentityInit {
assigned_user_number_range: None,
archive_config: None,
canister_creation_cycles_cost: None,
register_rate_limit: None,
captcha_config: None,
related_origins: Some(related_origins.clone()),
};
let canister_id = install_ii_canister_with_arg(&env, II_WASM.clone(), Some(config));

let request = HttpRequest {
method: "GET".to_string(),
url: "/.well-known/webauthn".to_string(),
headers: vec![],
body: ByteBuf::new(),
certificate_version: Some(2),
};
let http_response = http_request(&env, canister_id, &request)?;
let response_body = String::from_utf8_lossy(&http_response.body).to_string();
assert_eq!(http_response.status_code, 200);
let expected_content = json!({
"origins": related_origins,
})
.to_string();
assert_eq!(response_body, expected_content);

let _ = upgrade_ii_canister_with_arg(&env, canister_id, II_WASM.clone(), None);

let http_response_1 = http_request(&env, canister_id, &request)?;
let response_body_1 = String::from_utf8_lossy(&http_response_1.body).to_string();
assert_eq!(response_body_1, expected_content);

let related_origins_2: Vec<String> = [
"https://beta.identity.internetcomputer.org".to_string(),
"https://beta.identity.ic0.app".to_string(),
]
.to_vec();
let config_2 = InternetIdentityInit {
assigned_user_number_range: None,
archive_config: None,
canister_creation_cycles_cost: None,
register_rate_limit: None,
captcha_config: None,
related_origins: Some(related_origins_2.clone()),
};

let _ = upgrade_ii_canister_with_arg(&env, canister_id, II_WASM.clone(), Some(config_2));

let http_response_2 = http_request(&env, canister_id, &request)?;
let response_body_2 = String::from_utf8_lossy(&http_response_2.body).to_string();
let expected_content_2 = json!({
"origins": related_origins_2,
})
.to_string();
assert_eq!(response_body_2, expected_content_2);
Ok(())
}

/// Verifies that clients that do not indicate any certification version will get a v1 certificate.
#[test]
fn should_fallback_to_v1_certification() -> Result<(), CallError> {
Expand Down

0 comments on commit 4118aae

Please sign in to comment.