From a5cbaba2a3eabed33c52d05ba46e265496fd0800 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 12 Apr 2024 13:59:37 +0200 Subject: [PATCH 1/3] refactor: replace `ic-certified-map` with `ic-certification` --- Cargo.lock | 13 +- src/ic_backend/Cargo.toml | 2 +- src/ic_backend/src/delegation.rs | 39 ++--- src/ic_backend/src/hash.rs | 262 ------------------------------- src/ic_backend/src/lib.rs | 1 - 5 files changed, 11 insertions(+), 306 deletions(-) delete mode 100644 src/ic_backend/src/hash.rs diff --git a/Cargo.lock b/Cargo.lock index d170dcf..659ec9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1196,17 +1196,6 @@ dependencies = [ "sha2 0.10.8", ] -[[package]] -name = "ic-certified-map" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "197524aecec47db0b6c0c9f8821aad47272c2bd762c7a0ffe9715eaca0364061" -dependencies = [ - "serde", - "serde_bytes", - "sha2 0.10.8", -] - [[package]] name = "ic-representation-independent-hash" version = "2.5.0" @@ -1274,7 +1263,7 @@ dependencies = [ "ic-agent", "ic-cdk 0.13.1", "ic-cdk-timers", - "ic-certified-map", + "ic-certification", "ic-stable-structures", "ic_backend_types", "jsonwebtoken-rustcrypto", diff --git a/src/ic_backend/Cargo.toml b/src/ic_backend/Cargo.toml index e230d7b..b765905 100644 --- a/src/ic_backend/Cargo.toml +++ b/src/ic_backend/Cargo.toml @@ -13,7 +13,7 @@ candid.workspace = true ic-cdk = "0.13" ic-cdk-timers = "0.7" ic-stable-structures = "0.6" -ic-certified-map = "0.4" +ic-certification = "2.5" canister_sig_util = { git = "https://github.com/dfinity/internet-identity", tag = "release-2024-04-05" } serde.workspace = true diff --git a/src/ic_backend/src/delegation.rs b/src/ic_backend/src/delegation.rs index 4e5e379..bce8278 100644 --- a/src/ic_backend/src/delegation.rs +++ b/src/ic_backend/src/delegation.rs @@ -1,7 +1,6 @@ -use std::collections::HashMap; - use candid::Principal; use canister_sig_util::{ + delegation_signature_msg, hash_bytes, signature_map::{SignatureMap, LABEL_SIG}, CanisterSigPublicKey, }; @@ -10,10 +9,10 @@ use ic_backend_types::{ UserSub, }; use ic_cdk::{api::set_certified_data, id}; -use ic_certified_map::{labeled_hash, Hash}; +use ic_certification::{labeled_hash, Hash}; use serde_bytes::ByteBuf; -use crate::{hash, state}; +use crate::state; pub async fn prepare_delegation( user_sub: &UserSub, @@ -37,11 +36,7 @@ pub fn get_delegation( expiration: Timestamp, ) -> GetDelegationResponse { state::signature_map(|sigs| { - let message_hash = delegation_signature_msg_hash(&Delegation { - pubkey: session_key.clone(), - expiration, - targets: None, - }); + let message_hash = delegation_signature_msg_hash(&session_key, expiration); match sigs.get_signature_as_cbor(&calculate_seed(user_sub), message_hash, None) { Ok(signature) => GetDelegationResponse::SignedDelegation(SignedDelegation { delegation: Delegation { @@ -73,7 +68,7 @@ fn calculate_seed(user_sub: &UserSub) -> Hash { blob.push(user_sub_blob.len() as u8); blob.extend(user_sub_blob); - hash::hash_bytes(blob) + hash_bytes(blob) } fn update_root_hash() { @@ -83,21 +78,9 @@ fn update_root_hash() { }) } -fn delegation_signature_msg_hash(d: &Delegation) -> Hash { - use hash::Value; - - let mut m = HashMap::new(); - m.insert("pubkey", Value::Bytes(d.pubkey.as_slice())); - m.insert("expiration", Value::U64(d.expiration)); - if let Some(targets) = d.targets.as_ref() { - let mut arr = Vec::with_capacity(targets.len()); - for t in targets.iter() { - arr.push(Value::Bytes(t.as_ref())); - } - m.insert("targets", Value::Array(arr)); - } - let map_hash = hash::hash_of_map(m); - hash::hash_with_domain(b"ic-request-auth-delegation", &map_hash) +fn delegation_signature_msg_hash(pubkey: &PublicKey, expiration: Timestamp) -> Hash { + let msg = delegation_signature_msg(pubkey, expiration, None); + hash_bytes(msg) } fn add_delegation_signature( @@ -106,11 +89,7 @@ fn add_delegation_signature( seed: &[u8], expiration: Timestamp, ) { - let msg_hash = delegation_signature_msg_hash(&Delegation { - pubkey: pk, - expiration, - targets: None, - }); + let msg_hash = delegation_signature_msg_hash(&pk, expiration); sigs.add_signature(seed, msg_hash); } diff --git a/src/ic_backend/src/hash.rs b/src/ic_backend/src/hash.rs deleted file mode 100644 index c8b9f09..0000000 --- a/src/ic_backend/src/hash.rs +++ /dev/null @@ -1,262 +0,0 @@ -/// Utilities for computing hashes of values. -use ic_certified_map::Hash; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::collections::HashMap; -use std::convert::AsRef; - -/// Represents different types of values that can be hashed. -#[derive(Clone, Serialize, Deserialize)] -pub enum Value<'a> { - Bytes(#[serde(with = "serde_bytes")] &'a [u8]), - String(&'a str), - U64(u64), - Array(Vec>), -} - -/// Computes a hash of a map where keys are strings and values are `Value`. -pub(crate) fn hash_of_map>(map: HashMap) -> Hash { - let mut hashes = map - .into_iter() - .map(|(key, val)| hash_key_value(key.as_ref(), val)) - .collect::>(); - - hashes.sort_unstable(); - let mut hasher = Sha256::new(); - for hash in hashes { - hasher.update(&hash); - } - - hasher.finalize().into() -} - -/// Computes a hash with a domain separator. -pub(crate) fn hash_with_domain(sep: &[u8], bytes: &[u8]) -> Hash { - let mut hasher = Sha256::new(); - hasher.update([sep.len() as u8]); - hasher.update(sep); - hasher.update(bytes); - hasher.finalize().into() -} - -/// Helper function to hash a key and value pair. -fn hash_key_value(key: &str, val: Value<'_>) -> Vec { - let mut key_hash = hash_string(key).to_vec(); - let val_hash = hash_value(val); - key_hash.extend_from_slice(&val_hash[..]); - key_hash -} - -/// Hashes a string. -pub(crate) fn hash_string(value: &str) -> Hash { - hash_bytes(value.as_bytes()) -} - -/// Hashes a byte slice. -pub(crate) fn hash_bytes(value: impl AsRef<[u8]>) -> Hash { - let mut hasher = Sha256::new(); - hasher.update(value.as_ref()); - hasher.finalize().into() -} - -/// Hashes a 64-bit unsigned integer. -fn hash_u64(value: u64) -> Hash { - let mut buf = [0u8; 10]; - let mut n = value; - let mut i = 0; - - loop { - let byte = (n & 0x7f) as u8; - n >>= 7; - buf[i] = byte | if n != 0 { 0x80 } else { 0 }; - - if n == 0 { - break; - } - i += 1; - } - - hash_bytes(&buf[..=i]) -} - -/// Hashes an array of `Value`. -fn hash_array(elements: Vec>) -> Hash { - let mut hasher = Sha256::new(); - for element in elements { - hasher.update(&hash_value(element)[..]); - } - - hasher.finalize().into() -} - -/// Hashes a `Value`. -fn hash_value(val: Value<'_>) -> Hash { - match val { - Value::String(s) => hash_string(s), - Value::Bytes(b) => hash_bytes(b), - Value::U64(n) => hash_u64(n), - Value::Array(a) => hash_array(a), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use hex_literal::hex; - - #[test] - fn message_id_icf_key_val_reference_1() { - assert_eq!( - hash_key_value("request_type", Value::String("call")), - hex!( - " - 769e6f87bdda39c859642b74ce9763cdd37cb1cd672733e8c54efaa33ab78af9 - 7edb360f06acaef2cc80dba16cf563f199d347db4443da04da0c8173e3f9e4ed - " - ) - .to_vec() - ); - } - - #[test] - fn message_id_u64_id_reference() { - assert_eq!( - // LEB128: 0x00 - hash_u64(0), - hex!("6e340b9cffb37a989ca544e6bb780a2c78901d3fb33738768511a30617afa01d"), - ); - - assert_eq!( - // LEB128: 0xd2 0x09 - hash_u64(1234), - hex!("8b37fd3ebbe6396a89ed8563dd0cc55927ac90138950460c77cffeb55cf63810"), - ); - - assert_eq!( - // LEB128 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0xff 0x01 - hash_u64(0xffff_ffff_ffff_ffff), - hex!("51672ea45f3539654bf9193f4ff763d90022eee7df5f5b76353d6f11a9eaccec"), - ) - } - - #[test] - fn message_id_string_reference_1() { - assert_eq!( - hash_string("request_type"), - hex!("769e6f87bdda39c859642b74ce9763cdd37cb1cd672733e8c54efaa33ab78af9"), - ); - } - - #[test] - fn message_id_string_reference_2() { - assert_eq!( - hash_string("call"), - hex!("7edb360f06acaef2cc80dba16cf563f199d347db4443da04da0c8173e3f9e4ed"), - ); - } - - #[test] - fn message_id_string_reference_3() { - assert_eq!( - hash_string("callee"), - hex!("92ca4c0ced628df1e7b9f336416ead190bd0348615b6f71a64b21d1b68d4e7e2"), - ); - } - - #[test] - fn message_id_string_reference_4() { - assert_eq!( - hash_string("method_name"), - hex!("293536232cf9231c86002f4ee293176a0179c002daa9fc24be9bb51acdd642b6"), - ); - } - - #[test] - fn message_id_string_reference_5() { - assert_eq!( - hash_string("hello"), - hex!("2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"), - ); - } - - #[test] - fn message_id_string_reference_6() { - assert_eq!( - hash_string("arg"), - hex!("b25f03dedd69be07f356a06fe35c1b0ddc0de77dcd9066c4be0c6bbde14b23ff"), - ); - } - - #[test] - fn message_id_array_reference_1() { - assert_eq!( - hash_array(vec![Value::String("a")]), - // hash(hash("a")) - hex!("bf5d3affb73efd2ec6c36ad3112dd933efed63c4e1cbffcfa88e2759c144f2d8"), - ); - } - - #[test] - fn message_id_array_reference_2() { - assert_eq!( - hash_array(vec![Value::String("a"), Value::String("b"),]), - // hash(concat(hash("a"), hash("b")) - hex!("e5a01fee14e0ed5c48714f22180f25ad8365b53f9779f79dc4a3d7e93963f94a"), - ); - } - - #[test] - fn message_id_array_reference_3() { - assert_eq!( - hash_array(vec![ - Value::Bytes(&[97][..]), // "a" as a byte string. - Value::String("b"), - ]), - // hash(concat(hash("a"), hash("b")) - hex!("e5a01fee14e0ed5c48714f22180f25ad8365b53f9779f79dc4a3d7e93963f94a"), - ); - } - - #[test] - fn message_id_array_reference_4() { - assert_eq!( - hash_array(vec![Value::Array(vec![Value::String("a")])]), - // hash(hash(hash("a")) - hex!("eb48bdfa15fc43dbea3aabb1ee847b6e69232c0f0d9705935e50d60cce77877f"), - ); - } - - #[test] - fn message_id_array_reference_5() { - assert_eq!( - hash_array(vec![Value::Array(vec![ - Value::String("a"), - Value::String("b") - ])]), - // hash(hash(concat(hash("a"), hash("b"))) - hex!("029fd80ca2dd66e7c527428fc148e812a9d99a5e41483f28892ef9013eee4a19"), - ); - } - - #[test] - fn message_id_array_reference_6() { - assert_eq!( - hash_array(vec![ - Value::Array(vec![Value::String("a"), Value::String("b")]), - Value::Bytes(&[97][..]), // "a" in bytes - ]), - // hash(concat(hash(concat(hash("a"), hash("b")), hash(100)) - hex!("aec3805593d9ec6df50da070597f73507050ce098b5518d0456876701ada7bb7"), - ); - } - - #[test] - fn message_id_bytes_reference() { - assert_eq!( - // D I D L \0 \253 *" - // 68 73 68 76 0 253 42 - hash_bytes(&[68, 73, 68, 76, 0, 253, 42][..]), - hex!("6c0b2ae49718f6995c02ac5700c9c789d7b7862a0d53e6d40a73f1fcd2f70189") - ); - } -} diff --git a/src/ic_backend/src/lib.rs b/src/ic_backend/src/lib.rs index 6256a9b..f77b940 100644 --- a/src/ic_backend/src/lib.rs +++ b/src/ic_backend/src/lib.rs @@ -1,5 +1,4 @@ mod delegation; -mod hash; mod id_token; mod state; mod users; From 689cec5151aa65c3618947d6844ee037129ab27e Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 12 Apr 2024 14:20:29 +0200 Subject: [PATCH 2/3] test: verify canister signature --- Cargo.lock | 1 + src/ic_backend/Cargo.toml | 1 + src/ic_backend/tests/common/test_env.rs | 5 +- src/ic_backend/tests/delegation.rs | 65 +++++++++++++++++++------ 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 659ec9d..c40fead 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1264,6 +1264,7 @@ dependencies = [ "ic-cdk 0.13.1", "ic-cdk-timers", "ic-certification", + "ic-representation-independent-hash", "ic-stable-structures", "ic_backend_types", "jsonwebtoken-rustcrypto", diff --git a/src/ic_backend/Cargo.toml b/src/ic_backend/Cargo.toml index b765905..59195ea 100644 --- a/src/ic_backend/Cargo.toml +++ b/src/ic_backend/Cargo.toml @@ -34,3 +34,4 @@ pocket-ic = "2.2" jwt-simple = "0.12" ic-agent = "0.34" ring = "0.17" +ic-representation-independent-hash = "2.5" diff --git a/src/ic_backend/tests/common/test_env.rs b/src/ic_backend/tests/common/test_env.rs index 1c5e7de..ce784ca 100644 --- a/src/ic_backend/tests/common/test_env.rs +++ b/src/ic_backend/tests/common/test_env.rs @@ -12,7 +12,6 @@ use super::identity::generate_random_identity; pub struct TestEnv { pic: PocketIc, canister_id: Principal, - #[allow(dead_code)] root_ic_key: Vec, controller: Principal, } @@ -73,6 +72,10 @@ impl TestEnv { self.pic.tick(); } } + + pub fn root_ic_key(&self) -> &[u8] { + &self.root_ic_key + } } pub fn create_test_env() -> TestEnv { diff --git a/src/ic_backend/tests/delegation.rs b/src/ic_backend/tests/delegation.rs index bf3c946..5e87616 100644 --- a/src/ic_backend/tests/delegation.rs +++ b/src/ic_backend/tests/delegation.rs @@ -5,15 +5,16 @@ use std::time::SystemTime; use candid::Principal; use ic_agent::Identity; use ic_backend_types::{ - Delegation, GetDelegationResponse, PrepareDelegationResponse, SignedDelegation, + GetDelegationResponse, PrepareDelegationResponse, SignedDelegation, UserKey, }; +use ic_representation_independent_hash::{representation_independent_hash, Value}; use jwt_simple::prelude::*; use common::{ auth_provider::{create_jwt, initialize_auth_provider}, canister::{extract_trap_message, get_delegation, initialize_canister, prepare_delegation}, identity::{generate_random_identity, pk_to_hex}, - test_env::{create_test_env, upgrade_canister}, + test_env::{create_test_env, upgrade_canister, TestEnv}, }; const NANOS_IN_SECONDS: u64 = 1_000_000_000; @@ -23,6 +24,42 @@ const MAX_IAT_AGE_SECONDS: u64 = 10 * 60; // 10 minutes /// Same as on Auth0 const JWT_VALID_FOR_HOURS: u64 = 10; +fn verify_delegation( + env: &TestEnv, + user_key: UserKey, + signed_delegation: &SignedDelegation, + root_key: &[u8], +) { + const DOMAIN_SEPARATOR: &[u8] = b"ic-request-auth-delegation"; + + // The signed message is a signature domain separator + // followed by the representation independent hash of a map with entries + // pubkey, expiration and targets (if any), using the respective values from the delegation. + // See https://internetcomputer.org/docs/current/references/ic-interface-spec#authentication for details + let key_value_pairs = vec![ + ( + "pubkey".to_string(), + Value::Bytes(signed_delegation.delegation.pubkey.clone().into_vec()), + ), + ( + "expiration".to_string(), + Value::Number(signed_delegation.delegation.expiration), + ), + ]; + let mut msg: Vec = Vec::from([(DOMAIN_SEPARATOR.len() as u8)]); + msg.extend_from_slice(DOMAIN_SEPARATOR); + msg.extend_from_slice(&representation_independent_hash(&key_value_pairs)); + + env.pic() + .verify_canister_signature( + msg, + signed_delegation.signature.clone().into_vec(), + user_key.into_vec(), + root_key.to_vec(), + ) + .expect("delegation signature invalid"); +} + #[test] fn test_prepare_delegation() { let env = create_test_env(); @@ -234,24 +271,20 @@ fn test_get_delegation() { Duration::from_hours(JWT_VALID_FOR_HOURS), ); - let PrepareDelegationResponse { expiration, .. } = - prepare_delegation(&env, session_principal, jwt.clone()).unwrap(); + let PrepareDelegationResponse { + expiration, + user_key, + } = prepare_delegation(&env, session_principal, jwt.clone()).unwrap(); let res = get_delegation(&env, session_principal, jwt, expiration).unwrap(); match res { - GetDelegationResponse::SignedDelegation(SignedDelegation { - delegation: - Delegation { - targets, - pubkey, - expiration, - }, - .. - }) => { - assert_eq!(pubkey, session_public_key); - assert_eq!(expiration, expiration); - assert!(targets.is_none()); + GetDelegationResponse::SignedDelegation(signed_delegation) => { + assert_eq!(signed_delegation.delegation.pubkey, session_public_key); + assert_eq!(signed_delegation.delegation.expiration, expiration); + assert!(signed_delegation.delegation.targets.is_none()); + + verify_delegation(&env, user_key, &signed_delegation, env.root_ic_key()); } _ => panic!("Expected SignedDelegation"), } From e8ae4a63818d6aa4af8f41256db06a52a0d24a28 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Fri, 12 Apr 2024 14:22:31 +0200 Subject: [PATCH 3/3] chore: update readme --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bf1ecdb..43f26fb 100644 --- a/README.md +++ b/README.md @@ -189,16 +189,18 @@ The [canister_sig_util](https://github.com/dfinity/internet-identity/tree/releas ## Roadmap -- [x] on the canister, periodically fetch the [JSON Web Key Sets (JWKS)](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets) from Auth0 using the [HTTPS outcalls](https://internetcomputer.org/docs/current/references/https-outcalls-how-it-works/) and [Timers](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/advanced-features/periodic-tasks/) features. +- [x] On the canister, periodically fetch the [JSON Web Key Sets (JWKS)](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets) from Auth0 using the [HTTPS outcalls](https://internetcomputer.org/docs/current/references/https-outcalls-how-it-works/) and [Timers](https://internetcomputer.org/docs/current/developer-docs/smart-contracts/advanced-features/periodic-tasks/) features. Right now, the JWKS are fetched at build time by the [build-canister.sh](./scripts/build-canister.sh) script, stored in `data/jwks.json` and imported in the canister as raw bytes at compile time ([source](https://github.com/ilbertt/ic-react-native-jwt-auth/blob/882539addd4e0e35fe1f1756701296f1ff085239/src/ic_backend/src/id_token.rs#L12)). Fetching the JWKS at runtime is needed because [JWK](https://datatracker.ietf.org/doc/html/rfc7517)s on Auth0 may rotate. Related issue: https://github.com/ilbertt/ic-react-native-jwt-auth/issues/1. -- [x] tests (integration) +- [x] Integration tests - Related PR: https://github.com/ilbertt/ic-react-native-jwt-auth/pull/2. + Related PRs: + - https://github.com/ilbertt/ic-react-native-jwt-auth/pull/2 + - https://github.com/ilbertt/ic-react-native-jwt-auth/pull/3 ## License