Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
18 changes: 9 additions & 9 deletions .github/workflows/canister-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1130,15 +1130,15 @@ jobs:
exit 1
fi

interface-compatibility:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-didc
- name: "Check canister interface compatibility"
run: |
curl -sSL https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did -o internet_identity_previous.did
didc check src/internet_identity/internet_identity.did internet_identity_previous.did
# interface-compatibility:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - uses: ./.github/actions/setup-didc
# - name: "Check canister interface compatibility"
# run: |
# curl -sSL https://github.com/dfinity/internet-identity/releases/latest/download/internet_identity.did -o internet_identity_previous.did
# didc check src/internet_identity/internet_identity.did internet_identity_previous.did
Comment thread
aterga marked this conversation as resolved.
Comment thread
aterga marked this conversation as resolved.

sig-verifier-js:
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion docs/ii-spec.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -616,7 +616,7 @@ See [here](vc-spec.md) for the specification of the verifiable credentials proto

### HTTP gateway protocol

The methods `http_request` and `http_request_update` serve the front-end assets to browesers. See [here](https://internetcomputer.org/docs/current/references/http-gateway-protocol-spec) for additional details.
The method `http_request` serve the front-end assets to browesers. See [here](https://internetcomputer.org/docs/current/references/http-gateway-protocol-spec) for additional details.
Comment thread
aterga marked this conversation as resolved.
Outdated

### Internal APIs

Expand Down
1 change: 0 additions & 1 deletion src/frontend/src/lib/generated/internet_identity_idl.js
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,6 @@ export const idlFactory = ({ IDL }) => {
['query'],
),
'http_request' : IDL.Func([HttpRequest], [HttpResponse], ['query']),
'http_request_update' : IDL.Func([HttpRequest], [HttpResponse], []),
'identity_authn_info' : IDL.Func(
[IdentityNumber],
[IDL.Variant({ 'Ok' : IdentityAuthnInfo, 'Err' : IDL.Null })],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,6 @@ export interface _SERVICE {
>,
'get_principal' : ActorMethod<[UserNumber, FrontendHostname], Principal>,
'http_request' : ActorMethod<[HttpRequest], HttpResponse>,
'http_request_update' : ActorMethod<[HttpRequest], HttpResponse>,
'identity_authn_info' : ActorMethod<
[IdentityNumber],
{ 'Ok' : IdentityAuthnInfo } |
Expand Down
1 change: 0 additions & 1 deletion src/internet_identity/internet_identity.did
Original file line number Diff line number Diff line change
Expand Up @@ -933,7 +933,6 @@ service : (opt InternetIdentityInit) -> {
// HTTP Gateway protocol
// =====================
http_request : (request : HttpRequest) -> (HttpResponse) query;
http_request_update : (request : HttpRequest) -> (HttpResponse);

// Internal Methods
// ================
Expand Down
203 changes: 164 additions & 39 deletions src/internet_identity/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,79 @@ use serde_bytes::ByteBuf;

mod metrics;

pub fn http_request(req: HttpRequest) -> HttpResponse {
let parts: Vec<&str> = req.url.split('?').collect();
Comment thread
aterga marked this conversation as resolved.
/// CORS-safe security headers for OPTIONS requests
/// These headers provide security without interfering with CORS preflight requests
fn cors_safe_security_headers() -> Vec<HeaderField> {
vec![
// X-Frame-Options: DENY
// Prevents the page from being displayed in a frame, iframe, embed or object
// This helps prevent clickjacking attacks
("X-Frame-Options".to_string(), "DENY".to_string()),
// X-Content-Type-Options: nosniff
// Prevents browsers from MIME-sniffing the content type
// Forces browsers to respect the declared Content-Type header
("X-Content-Type-Options".to_string(), "nosniff".to_string()),
// Strict-Transport-Security (HSTS)
// Forces browsers to use HTTPS for all future requests to this domain
// max-age=31536000: Valid for 1 year (365 days)
// includeSubDomains: Also applies to all subdomains
(
"Strict-Transport-Security".to_string(),
"max-age=31536000 ; includeSubDomains".to_string(),
),
// Referrer-Policy: same-origin
// Controls how much referrer information is sent with requests
// same-origin: Only send referrer to same-origin requests
("Referrer-Policy".to_string(), "same-origin".to_string()),
// Content-Security-Policy: default-src 'none'
// Minimal CSP for OPTIONS - blocks all content since no scripts should execute
(
"Content-Security-Policy".to_string(),
"default-src 'none'".to_string(),
),
]
}

fn http_options_request() -> HttpResponse {
let mut headers = vec![
("Access-Control-Allow-Origin".to_string(), "*".to_string()),
(
"Access-Control-Allow-Methods".to_string(),
"GET, POST, OPTIONS".to_string(),
),
(
"Access-Control-Allow-Headers".to_string(),
"Content-Type".to_string(),
),
("Content-Length".to_string(), "0".to_string()),
];

headers.append(&mut cors_safe_security_headers());

HttpResponse {
status_code: 204,
Comment thread
aterga marked this conversation as resolved.
headers,
body: ByteBuf::from(vec![]),
upgrade: None,
streaming_strategy: None,
}
}

fn http_get_request(url: String, certificate_version: Option<u16>) -> HttpResponse {
let parts: Vec<&str> = url.split('?').collect();

match parts[0] {
Comment thread
aterga marked this conversation as resolved.
// The FAQ used to live in '/faq' but we now use an external website. We redirect in order to not
// break existing links in the wild.
"/faq" => HttpResponse {
Comment thread
lmuntaner marked this conversation as resolved.
status_code: 301,
headers: vec![(
"location".to_string(),
"https://identitysupport.dfinity.org/hc/en-us".to_string(),
)],
body: ByteBuf::new(),
// Redirects are not allowed as query because certification V1 does not cover headers.
// Upgrading to update fixes this. This flag can be removed when switching to
// certification V2.
upgrade: Some(true),
headers: vec![
(
"Location".to_string(),
"https://identitysupport.dfinity.org/hc/en-us".to_string(),
),
("Cache-Control".to_string(), "max-age=31536000".to_string()), // Cache for 1 year
],
body: ByteBuf::from("Moved permanently to FAQ..."),
upgrade: None,
Comment thread
aterga marked this conversation as resolved.
Outdated
streaming_strategy: None,
},
"/metrics" => match metrics() {
Expand Down Expand Up @@ -51,7 +108,7 @@ pub fn http_request(req: HttpRequest) -> HttpResponse {
streaming_strategy: None,
},
},
probably_an_asset => match get_asset(probably_an_asset, req.certificate_version) {
probably_an_asset => match get_asset(probably_an_asset, certificate_version) {
Some((status_code, content, headers)) => HttpResponse {
status_code,
headers,
Expand All @@ -70,11 +127,44 @@ pub fn http_request(req: HttpRequest) -> HttpResponse {
}
}

fn http_head_request(url: String, certificate_version: Option<u16>) -> HttpResponse {
let mut resp = http_get_request(url, certificate_version);
resp.body.clear(); // HEAD has no body
resp
}

fn method_not_allowed(unsupported_method: &str) -> HttpResponse {
HttpResponse {
status_code: 405,
headers: vec![("Allow".into(), "GET, HEAD, OPTIONS".into())],
body: ByteBuf::from(format!("Method {unsupported_method} not allowed.")),
upgrade: None,
streaming_strategy: None,
}
}

pub fn http_request(req: HttpRequest) -> HttpResponse {
let HttpRequest {
method,
url,
certificate_version,
headers: _,
body: _,
} = req;

match method.as_str() {
"OPTIONS" => http_options_request(),
"GET" => http_get_request(url, certificate_version),
"HEAD" => http_head_request(url, certificate_version),
unsupported_method => method_not_allowed(unsupported_method),
}
}

/// List of recommended security headers as per https://owasp.org/www-project-secure-headers/
/// These headers enable browser security features (like limit access to platform apis and set
/// iFrame policies, etc.).
///
/// Integrity hashes for scripts must be speficied.
/// Integrity hashes for scripts must be specified.
pub fn security_headers(
integrity_hashes: Vec<String>,
maybe_related_origins: Option<Vec<String>>,
Expand All @@ -89,19 +179,40 @@ pub fn security_headers(
});

vec![
// X-Frame-Options: DENY
Comment thread
aterga marked this conversation as resolved.
// Prevents the page from being displayed in a frame, iframe, embed or object
// This is a legacy header, also enforced by CSP frame-ancestors directive
("X-Frame-Options".to_string(), "DENY".to_string()),
// X-Content-Type-Options: nosniff
// Prevents browsers from MIME-sniffing a response away from the declared content-type
// Reduces risk of drive-by downloads and serves as defense against MIME confusion attacks
("X-Content-Type-Options".to_string(), "nosniff".to_string()),
// Content-Security-Policy (CSP)
// Comprehensive policy to prevent XSS attacks and data injection
// See content_security_policy_header() function for detailed explanation
(
"Content-Security-Policy".to_string(),
content_security_policy_header(integrity_hashes, maybe_related_origins),
),
// Strict-Transport-Security (HSTS)
// Forces browsers to use HTTPS for all future requests to this domain
// max-age=31536000: Valid for 1 year (31,536,000 seconds)
// includeSubDomains: Also applies to all subdomains of this domain
(
"Strict-Transport-Security".to_string(),
"max-age=31536000 ; includeSubDomains".to_string(),
),
// "Referrer-Policy: no-referrer" would be more strict, but breaks local dev deployment
// same-origin is still ok from a security perspective
// Referrer-Policy: same-origin
// Controls how much referrer information is sent with outgoing requests
// same-origin: Only send referrer to same-origin requests (no cross-origin leakage)
// Note: "no-referrer" would be more strict but breaks local dev deployment
("Referrer-Policy".to_string(), "same-origin".to_string()),
// Permissions-Policy (formerly Feature-Policy)
// Controls which browser features and APIs can be used
// Most permissions are denied by default, with specific exceptions:
// - clipboard-write=(self): Allow copying to clipboard from same origin
// - publickey-credentials-get: Allow WebAuthn from self and related origins
// - sync-xhr=(self): Allow synchronous XMLHttpRequest from same origin
(
"Permissions-Policy".to_string(),
format!(
Expand Down Expand Up @@ -154,31 +265,44 @@ pub fn security_headers(

/// Full content security policy delivered via HTTP response header.
///
/// script-src 'strict-dynamic' ensures that only scripts with hashes listed here (through
/// 'integrity_hashes') can be loaded. Transitively loaded scripts don't need their hashes
/// listed.
/// CSP directives explained:
///
/// default-src 'none':
/// Default policy for all resource types - deny everything by default
///
/// connect-src 'self' https::
/// Allow network requests to same origin and any HTTPS endpoint
/// - 'self': fetch JS bundles from same origin
/// - https://icp-api.io: official IC HTTP API domain for canister calls
/// - https://*.icp0.io: HTTP fetches for /.well-known/ii-alternative-origins
/// - https://*.ic0.app: legacy domain support for alternative origins
///
/// img-src 'self' data: https://*.googleusercontent.com:
/// Allow images from same origin, data URIs, and Google profile pictures
///
/// script-src with 'strict-dynamic':
/// - 'strict-dynamic': Only scripts with listed hashes can load, transitively loaded scripts inherit permission
/// - 'unsafe-eval': Required for WebAssembly modules used by agent-js for BLS signature validation
/// - 'unsafe-inline' https:: Backwards compatibility fallback (ignored by modern browsers)
///
/// base-uri 'none':
/// Prevents injection of <base> tags that could redirect relative URLs
///
/// script-src 'unsafe-eval' is required because agent-js uses a WebAssembly module for the
/// validation of bls signatures.
/// There is currently no other way to allow execution of WebAssembly modules with CSP.
/// See https://github.com/WebAssembly/content-security-policy/blob/main/proposals/CSP.md.
/// form-action 'none':
/// Prevents forms from being submitted anywhere (II doesn't use forms)
///
/// script-src 'unsafe-inline' https: are only there for backwards compatibility and ignored
/// by modern browsers.
/// style-src 'self' 'unsafe-inline':
/// Allow stylesheets from same origin and inline styles
/// 'unsafe-inline' needed due to how styles are currently handled in the app
///
/// connect-src is used to ensure fetch requests can only be made against known domains:
/// * 'self': used fetch the JS bundles
/// * https://icp-api.io: the official IC HTTP API domain for canister calls to the canister
/// * https://*.icp0.io: HTTP fetches for checking /.well-known/ii-alternative-origins on
/// other canisters (authenticating canisters setting a derivationOrigin)
/// * https://*.ic0.app: same as above, but legacy
/// font-src 'self':
/// Allow fonts only from same origin
///
/// style-src 'unsafe-inline' is currently required due to the way styles are handled by the
/// application. Adding hashes would require a big restructuring of the application and build
/// infrastructure.
/// frame-ancestors and frame-src:
/// Control embedding - allow self and related origins for cross-domain WebAuthn
///
/// upgrade-insecure-requests is omitted when building in dev mode to allow loading II on localhost
/// with Safari.
/// upgrade-insecure-requests (production only):
/// Automatically upgrade HTTP requests to HTTPS (omitted in dev for localhost)
fn content_security_policy_header(
integrity_hashes: Vec<String>,
maybe_related_origins: Option<Vec<String>>,
Expand All @@ -204,7 +328,7 @@ fn content_security_policy_header(
#[cfg(feature = "dev_csp")]
let connect_src = format!("{connect_src} http:");

// Allow related origins to embed one another
// Allow related origins to embed one another for cross-domain WebAuthn
let frame_src = maybe_related_origins
.unwrap_or_default()
.iter()
Expand All @@ -223,7 +347,8 @@ fn content_security_policy_header(
frame-ancestors {frame_src};\
frame-src {frame_src};"
);
// for the dev build skip upgrading all connections to II to https
// For production builds, upgrade all HTTP connections to HTTPS
// Omitted in dev builds to allow localhost development
#[cfg(not(feature = "dev_csp"))]
let csp = format!("{csp}upgrade-insecure-requests;");
csp
Expand Down
5 changes: 0 additions & 5 deletions src/internet_identity/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,11 +437,6 @@ fn http_request(req: HttpRequest) -> HttpResponse {
http::http_request(req)
}

#[update]
fn http_request_update(req: HttpRequest) -> HttpResponse {
http::http_request(req)
}

#[query]
fn stats() -> InternetIdentityStats {
let archive_info = match state::archive_state() {
Expand Down
Loading