diff --git a/Cargo.lock b/Cargo.lock index 6d5c22a191..d6835e4334 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ dependencies = [ "ic-cdk 0.16.0", "ic-cdk-macros 0.16.0", "ic-certification 2.6.0", - "ic-representation-independent-hash", + "ic-representation-independent-hash 2.6.0", "include_dir", "internet_identity_interface", "lazy_static", @@ -360,6 +360,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bstr" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -489,7 +499,7 @@ dependencies = [ "flate2", "hex", "ic-cdk 0.16.0", - "ic-representation-independent-hash", + "ic-representation-independent-hash 2.6.0", "ic-verifiable-credentials", "identity_jose", "internet_identity_interface", @@ -1216,6 +1226,19 @@ version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" +[[package]] +name = "globset" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax 0.8.4", +] + [[package]] name = "group" version = "0.10.0" @@ -1477,6 +1500,19 @@ dependencies = [ "tracing", ] +[[package]] +name = "ic-asset-certification" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c43805022a5e4f408de44ca26396128697a2c39f83f4fdaf33f8aa3ac653d78e" +dependencies = [ + "globset", + "http 1.1.0", + "ic-certification 3.0.3", + "ic-http-certification 3.0.3", + "thiserror 1.0.63", +] + [[package]] name = "ic-canister-sig-creation" version = "1.1.0" @@ -1487,7 +1523,7 @@ dependencies = [ "hex", "ic-cdk 0.14.1", "ic-certification 2.6.0", - "ic-representation-independent-hash", + "ic-representation-independent-hash 2.6.0", "lazy_static", "serde", "serde_bytes", @@ -1657,12 +1693,29 @@ dependencies = [ "candid", "http 0.2.12", "ic-certification 2.6.0", - "ic-representation-independent-hash", + "ic-representation-independent-hash 2.6.0", "serde", "thiserror 1.0.63", "urlencoding", ] +[[package]] +name = "ic-http-certification" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d1a65b0ffb568e954750067f660e254f4564394f5c064a88e0e93b2eea4a532" +dependencies = [ + "base64 0.22.1", + "candid", + "http 1.1.0", + "ic-certification 3.0.3", + "ic-representation-independent-hash 3.0.3", + "serde", + "serde_cbor", + "thiserror 1.0.63", + "urlencoding", +] + [[package]] name = "ic-management-canister-types" version = "0.3.2" @@ -1690,6 +1743,16 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "ic-representation-independent-hash" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2800ba4910f21d9e1cb7b6ecbbbb0f76074bd2e127b4688c57d0936206caa6e" +dependencies = [ + "leb128", + "sha2 0.10.9", +] + [[package]] name = "ic-response-verification" version = "2.6.0" @@ -1704,8 +1767,8 @@ dependencies = [ "ic-cbor", "ic-certificate-verification", "ic-certification 2.6.0", - "ic-http-certification", - "ic-representation-independent-hash", + "ic-http-certification 2.6.0", + "ic-representation-independent-hash 2.6.0", "leb128", "log", "nom", @@ -2047,12 +2110,13 @@ dependencies = [ "flate2", "getrandom 0.2.15", "hex", + "ic-asset-certification", "ic-canister-sig-creation", "ic-cdk 0.16.0", "ic-cdk-macros 0.16.0", "ic-cdk-timers", "ic-certification 2.6.0", - "ic-http-certification", + "ic-http-certification 3.0.3", "ic-metrics-encoder", "ic-response-verification", "ic-stable-structures", @@ -2083,6 +2147,7 @@ version = "0.1.0" dependencies = [ "candid", "ic-cdk 0.16.0", + "ic-http-certification 3.0.3", "serde", "serde_bytes", ] diff --git a/Cargo.toml b/Cargo.toml index 524f703df6..510cd1444f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,8 @@ ic-cdk = "0.16" ic-cdk-macros = "0.16" ic-cdk-timers = "0.10" ic-certification = "2.6" -ic-http-certification = "2.6" +ic-http-certification = "3.0.3" +ic-asset-certification = "3.0.3" ic-metrics-encoder = "1" ic-representation-independent-hash = "2.6" ic-response-verification = "2.6" diff --git a/src/internet_identity/Cargo.toml b/src/internet_identity/Cargo.toml index 7c86414a53..78baedf4ab 100644 --- a/src/internet_identity/Cargo.toml +++ b/src/internet_identity/Cargo.toml @@ -41,12 +41,13 @@ ic-metrics-encoder.workspace = true ic-stable-structures.workspace = true ic-canister-sig-creation.workspace = true ic-verifiable-credentials.workspace = true +ic-http-certification.workspace = true +ic-asset-certification.workspace = true [target.'cfg(all(target_arch = "wasm32", target_vendor = "unknown", target_os = "unknown"))'.dependencies] getrandom = { version = "0.2", features = ["custom"] } [dev-dependencies] -ic-http-certification.workspace = true pocket-ic.workspace = true candid_parser.workspace = true canister_tests.workspace = true diff --git a/src/internet_identity/src/assets.rs b/src/internet_identity/src/assets.rs index 6254c2806d..b54064bad3 100644 --- a/src/internet_identity/src/assets.rs +++ b/src/internet_identity/src/assets.rs @@ -2,18 +2,62 @@ // // This file describes which assets are used and how (content, content type and content encoding). use crate::http::security_headers; -use crate::state; +use crate::state::{ + self, CertifiedHttpResponse, ASSET_CEL_EXPR_DEF, HTTP_TREE, OPTIONS_REQUEST_PATH, + OPTIONS_TREE_PATH, RESPONSES, +}; use asset_util::{collect_assets, Asset, CertifiedAssets, ContentEncoding, ContentType}; use base64::engine::general_purpose::STANDARD as BASE64; use base64::Engine; use candid::Encode; use ic_cdk::api; +use ic_http_certification::{ + HttpCertification, HttpCertificationTreeEntry, StatusCode, CERTIFICATE_EXPRESSION_HEADER_NAME, +}; use include_dir::{include_dir, Dir}; use internet_identity_interface::internet_identity::types::InternetIdentityInit; use serde_json::json; use sha2::Digest; -// used both in init and post_upgrade +pub fn certify_options_response() { + // TODO: Restrict origin to just the II-specific origins. + let headers = vec![ + ("Access-Control-Allow-Origin".to_string(), "*".to_string()), + ( + CERTIFICATE_EXPRESSION_HEADER_NAME.to_string(), + ASSET_CEL_EXPR_DEF.to_string(), + ), + ]; + + let response = ic_http_certification::HttpResponseBuilder::new() + .with_status_code(StatusCode::NO_CONTENT) + .with_headers(headers) + .build(); + + let certification = + HttpCertification::response_only(&ASSET_CEL_EXPR_DEF, &response, None).unwrap(); + + HTTP_TREE.with_borrow_mut(|http_tree| { + // add the certification to the certification tree + http_tree.insert(&HttpCertificationTreeEntry::new( + &*OPTIONS_TREE_PATH, + &certification, + )); + }); + + RESPONSES.with_borrow_mut(|responses| { + // store the response for later retrieval + responses.insert( + OPTIONS_REQUEST_PATH.to_string(), + CertifiedHttpResponse { + response, + certification, + }, + ); + }); +} + +/// Used in both http_options_requesthttp_options_request and post_upgrade. pub fn init_assets(config: &InternetIdentityInit) { state::assets_mut(|certified_assets| { let assets = get_static_assets(config); @@ -38,6 +82,8 @@ pub fn init_assets(config: &InternetIdentityInit) { &security_headers(integrity_hashes, config.related_origins.clone()), ); }); + + certify_options_response(); } // Fix up HTML pages, by injecting canister ID and canister config @@ -87,6 +133,14 @@ pub fn get_static_assets(config: &InternetIdentityInit) -> Vec { content_type: ContentType::OCTETSTREAM, }); + // Special asset for responding to OPTIONS requests. + // assets.push(Asset { + // url_path: "/".to_string(), + // content: vec![], + // encoding: ContentEncoding::Identity, + // content_type: ContentType::OCTETSTREAM, + // }); + if let Some(related_origins) = &config.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 diff --git a/src/internet_identity/src/http.rs b/src/internet_identity/src/http.rs index cac29d0ff6..43e40b1b5f 100644 --- a/src/internet_identity/src/http.rs +++ b/src/internet_identity/src/http.rs @@ -1,24 +1,47 @@ use crate::http::metrics::metrics; -use crate::state; +use crate::state::{ + self, CertifiedHttpResponse, HTTP_TREE, OPTIONS_REQUEST_PATH, OPTIONS_TREE_PATH, RESPONSES, +}; use ic_canister_sig_creation::signature_map::LABEL_SIG; +use ic_cdk::api::data_certificate; +use ic_cdk::trap; use ic_certification::{labeled_hash, pruned}; +use ic_http_certification::utils::add_v2_certificate_header; +use ic_http_certification::HttpCertificationTreeEntry; use internet_identity_interface::http_gateway::{HeaderField, HttpRequest, HttpResponse}; use serde_bytes::ByteBuf; mod metrics; fn http_options_request() -> HttpResponse { - // TODO: Restrict origin to just the II-specific origins. - let headers = vec![("Access-Control-Allow-Origin".to_string(), "*".to_string())]; + let Some(CertifiedHttpResponse { + mut response, + certification, + }) = RESPONSES.with_borrow(|responses| responses.get(*OPTIONS_REQUEST_PATH).cloned()) + else { + trap("OPTIONS response not found"); + }; - HttpResponse { - // Indicates success without any additional content to be sent in the response content. - status_code: 204, - headers, - body: ByteBuf::from(vec![]), - upgrade: None, - streaming_strategy: None, - } + HTTP_TREE.with_borrow(|http_tree| { + add_v2_certificate_header( + &data_certificate().expect("No data certificate available"), + &mut response, + &http_tree + .witness( + &HttpCertificationTreeEntry::new(&*OPTIONS_TREE_PATH, certification), + &*OPTIONS_REQUEST_PATH, + ) + .unwrap_or_else(|err| { + trap(&format!( + "Failed to create witness for OPTIONS response: {:?}", + err + )) + }), + &*OPTIONS_TREE_PATH.to_expr_path(), + ); + }); + + HttpResponse::from(response) } fn http_get_request(url: String, certificate_version: Option) -> HttpResponse { @@ -81,17 +104,9 @@ fn method_not_allowed(unsupported_method: &str) -> HttpResponse { } pub fn http_request(req: HttpRequest) -> HttpResponse { - let HttpRequest { - method, - url, - certificate_version, - headers: _, - body: _, - } = req; - - match method.as_str() { + match req.method.as_str() { "OPTIONS" => http_options_request(), - "GET" => http_get_request(url, certificate_version), + "GET" => http_get_request(req.url, req.certificate_version), unsupported_method => method_not_allowed(unsupported_method), } } diff --git a/src/internet_identity/src/state.rs b/src/internet_identity/src/state.rs index df059f994f..66cc0ef01a 100644 --- a/src/internet_identity/src/state.rs +++ b/src/internet_identity/src/state.rs @@ -14,8 +14,13 @@ use asset_util::CertifiedAssets; use candid::{CandidType, Deserialize, Principal}; use ic_canister_sig_creation::signature_map::SignatureMap; use ic_cdk::trap; +use ic_http_certification::{ + DefaultCelBuilder, DefaultResponseCertification, DefaultResponseOnlyCelExpression, + HttpCertification, HttpCertificationPath, HttpCertificationTree, HttpResponse, +}; use ic_stable_structures::DefaultMemoryImpl; use internet_identity_interface::internet_identity::types::*; +use lazy_static::lazy_static; use std::cell::{Cell, RefCell}; use std::collections::HashMap; use std::ops::{Deref, DerefMut}; @@ -36,9 +41,30 @@ pub const DEFAULT_RATE_LIMIT_CONFIG: RateLimitConfig = RateLimitConfig { max_tokens: 20_000, }; +#[derive(Clone)] +pub(crate) struct CertifiedHttpResponse<'a> { + pub response: HttpResponse<'a>, + pub certification: HttpCertification, +} + thread_local! { static STATE: State = State::default(); static ASSETS: RefCell = RefCell::new(CertifiedAssets::default()); + + pub(crate) static HTTP_TREE: RefCell = RefCell::new(HttpCertificationTree::default()); + pub(crate) static RESPONSES: RefCell>> = RefCell::new(HashMap::new()); +} + +lazy_static! { + pub(crate) static ref ASSET_CEL_EXPR_DEF: DefaultResponseOnlyCelExpression<'static> = + DefaultCelBuilder::response_only_certification() + .with_response_certification(DefaultResponseCertification::response_header_exclusions( + vec![], + )) + .build(); + pub(crate) static ref OPTIONS_REQUEST_PATH: &'static str = ""; + pub(crate) static ref OPTIONS_TREE_PATH: HttpCertificationPath<'static> = + HttpCertificationPath::wildcard(*OPTIONS_REQUEST_PATH); } #[derive(Clone)] diff --git a/src/internet_identity_interface/Cargo.toml b/src/internet_identity_interface/Cargo.toml index c98ae82d8c..411136b4a1 100644 --- a/src/internet_identity_interface/Cargo.toml +++ b/src/internet_identity_interface/Cargo.toml @@ -10,3 +10,4 @@ serde_bytes.workspace = true candid.workspace = true serde.workspace = true ic-cdk.workspace = true +ic-http-certification.workspace = true diff --git a/src/internet_identity_interface/src/http_gateway.rs b/src/internet_identity_interface/src/http_gateway.rs index 2784678879..4fc261a272 100644 --- a/src/internet_identity_interface/src/http_gateway.rs +++ b/src/internet_identity_interface/src/http_gateway.rs @@ -2,6 +2,7 @@ //! See https://internetcomputer.org/docs/current/references/ic-interface-spec/#http-gateway-interface use candid::{define_function, CandidType, Deserialize}; +use ic_http_certification; use serde_bytes::ByteBuf; pub type HeaderField = (String, String); @@ -34,6 +35,37 @@ pub struct HttpRequest { pub certificate_version: Option, } +impl TryFrom for ic_http_certification::HttpRequest<'_> { + type Error = String; + + fn try_from(value: HttpRequest) -> Result { + use ic_http_certification::{HttpRequestBuilder, Method}; + + let HttpRequest { + method, + url, + headers, + body, + certificate_version, + } = value; + + let method = Method::from_bytes(method.as_bytes()) + .map_err(|_| format!("Unexpected method {}", method))?; + + let mut request = HttpRequestBuilder::new() + .with_method(method) + .with_url(url) + .with_headers(headers) + .with_body(body.into_vec()); + + if let Some(certificate_version) = certificate_version { + request = request.with_certificate_version(certificate_version); + } + + Ok(request.build()) + } +} + #[derive(Clone, Debug, CandidType, Deserialize)] pub struct HttpResponse { pub status_code: u16, @@ -42,3 +74,20 @@ pub struct HttpResponse { pub upgrade: Option, pub streaming_strategy: Option, } + +impl From> for HttpResponse { + fn from(value: ic_http_certification::HttpResponse<'_>) -> Self { + let body = ByteBuf::from(value.body()); + let status_code = value.status_code().into(); + let upgrade = value.upgrade(); + let headers = value.headers().iter().cloned().collect::>(); + + HttpResponse { + status_code, + headers, + body, + upgrade, + streaming_strategy: None, + } + } +}