Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions Cargo.lock

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

170 changes: 169 additions & 1 deletion rs/tests/crypto/ingress_verification_test.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* tag::catalog[]
end::catalog[] */
use anyhow::Result;
use candid::Encode;
use candid::{CandidType, Encode};
use ic_agent::Identity;
use ic_agent::export::Principal;
use ic_crypto_test_utils_reproducible_rng::reproducible_rng;
Expand All @@ -28,6 +28,7 @@ use ic_universal_canister::wasm;
use rand::{CryptoRng, Rng, SeedableRng, rngs::StdRng};
use reqwest::{StatusCode, Url};
use slog::debug;
use std::collections::BTreeMap;

const ALL_QUERY_API_VERSIONS: &[usize] = &[2, 3];
const ALL_UPDATE_API_VERSIONS: &[usize] = &[2, 3, 4];
Expand All @@ -47,6 +48,7 @@ fn main() -> Result<()> {
.add_test(systest!(requests_to_mgmt_canister_with_delegations))
.add_test(systest!(requests_with_sender_info))
.add_test(systest!(requests_with_valid_sender_info))
.add_test(systest!(requests_with_sender_info_permissions))
.add_test(systest!(requests_with_invalid_expiry))
.add_test(systest!(requests_with_canister_signature)),
)
Expand Down Expand Up @@ -1353,6 +1355,172 @@ pub fn requests_with_valid_sender_info(env: TestEnv) {
});
}

/// Minimal mirror of the ICRC-3 `Value` type used to encode the attributes
/// map carried in `sender_info.info`, like the mirror used by Internet
/// Identity to certify request attributes.
#[derive(CandidType)]
enum Icrc3Value {
Text(String),
Map(BTreeMap<String, Icrc3Value>),
}

/// Candid-encodes an ICRC-3 attributes map, optionally containing the
/// `implicit:permissions` attribute with the given value.
fn encoded_attributes_with_permissions(permissions: Option<&str>) -> Vec<u8> {
let mut attributes = BTreeMap::from([(
"implicit:origin".to_string(),
Icrc3Value::Text("https://example.com".to_string()),
)]);
if let Some(permissions) = permissions {
attributes.insert(
"implicit:permissions".to_string(),
Icrc3Value::Text(permissions.to_string()),
);
}
Encode!(&Icrc3Value::Map(attributes)).expect("failed to encode attributes")
}

/// Demonstrates the `implicit:permissions` sender_info attribute end-to-end:
/// a signer canister (playing the role of Internet Identity) certifies an
/// ICRC-3 attributes map restricting the sender to `"queries"`, and the
/// protocol then rejects the sender's update calls during ingress validation
/// while still permitting query calls (where the receiving canister can also
/// read the certified attributes). Attribute maps with permissions
/// `"updates"`, or without the permissions attribute, leave update calls
/// permitted.
pub fn requests_with_sender_info_permissions(env: TestEnv) {
let logger = env.logger();
let node = env.get_first_healthy_node_snapshot();
let agent = node.build_default_agent();
block_on({
async move {
let node_url = node.get_public_url();
debug!(logger, "Selected replica"; "url" => format!("{}", node_url));

let canister =
UniversalCanister::new_with_retries(&agent, node.effective_canister_id(), &logger)
.await;
let test_info = TestInformation {
url: node_url,
canister_id: canister_id_from_principal(&canister.canister_id()),
};

let seed = b"sender_info_permissions_test_seed".to_vec();
let signer = CanisterSigner::new(&canister, seed);
let id = GenericIdentity::new_canister(signer.clone());

// Helper: canister-sign the given attributes blob as a
// sender_info, like Internet Identity does when certifying
// request attributes.
let signed_sender_info = |info_bytes: Vec<u8>| {
let signer = signer.clone();
async move {
let sig = signer
.sign(&SenderInfoContent(&info_bytes).as_signed_bytes())
.await;
RawSignedSenderInfo {
info: Blob(info_bytes),
signer: Blob(signer.canister_id().get().as_slice().to_vec()),
sig: Blob(sig),
}
}
};

// Helper: build an update call with the given sender_info, compute
// the message ID from ic-types (which includes sender_info, unlike
// ic_agent::EnvelopeContent), sign it, and send.
let send_update = |api_ver, sender_info| {
let content = HttpCallContent::Call {
update: HttpCanisterUpdate {
canister_id: Blob(test_info.canister_id.get().as_slice().to_vec()),
method_name: "update".to_string(),
arg: Blob(wasm().reply_data(b"update_reply").build()),
sender: Blob(id.principal().as_slice().to_vec()),
ingress_expiry: expiry_time().as_nanos() as u64,
nonce: None,
sender_info: Some(sender_info),
},
};
let message_id = MessageId::from(content.representation_independent_hash());
let signature = id.sign_bytes(&message_id.as_signed_bytes());
let fut = send_request(
api_ver,
&test_info,
"call",
content,
id.public_key_der(),
None,
signature,
);
async move { (fut.await, message_id) }
};

let queries_only_info = encoded_attributes_with_permissions(Some("queries"));
let queries_only_sender_info = signed_sender_info(queries_only_info.clone()).await;

///////////////////////////////////////////////////////////////////
// A sender restricted to "queries" must not be able to make
// update calls: the protocol rejects them during ingress
// validation.
for &api_ver in ALL_UPDATE_API_VERSIONS {
let (response, _) = send_update(api_ver, queries_only_sender_info.clone()).await;
assert_eq!(response.status(), 400);
response.expect_text_error("Sender info does not permit update calls");
}

///////////////////////////////////////////////////////////////////
// The same sender_info still permits query calls, where the
// receiving canister can read the certified attributes via the
// msg_caller_info system API.
for &api_ver in ALL_QUERY_API_VERSIONS {
let content = HttpQueryContent::Query {
query: HttpUserQuery {
canister_id: Blob(test_info.canister_id.get().as_slice().to_vec()),
method_name: "query".to_string(),
arg: Blob(wasm().msg_caller_info_data().append_and_reply().build()),
sender: Blob(id.principal().as_slice().to_vec()),
ingress_expiry: expiry_time().as_nanos() as u64,
nonce: None,
sender_info: Some(queries_only_sender_info.clone()),
},
};
let message_id = MessageId::from(content.representation_independent_hash());
let signature = id.sign_bytes(&message_id.as_signed_bytes());
let response = send_request(
api_ver,
&test_info,
"query",
content,
id.public_key_der(),
None,
signature,
)
.await;
response.expect_query_ok(api_ver);
response.expect_query_reply_arg(api_ver, &queries_only_info);
}

///////////////////////////////////////////////////////////////////
// Attributes with permissions "updates", or without the
// permissions attribute, leave update calls permitted.
for info_bytes in [
encoded_attributes_with_permissions(Some("updates")),
encoded_attributes_with_permissions(None),
] {
let sender_info = signed_sender_info(info_bytes).await;
for &api_ver in ALL_UPDATE_API_VERSIONS {
let (response, message_id) = send_update(api_ver, sender_info.clone()).await;
let response =
await_pending_update(api_ver, response, &test_info, &id, &message_id).await;
response
.with_request_id(message_id)
.expect_update_ok(api_ver);
}
}
}
});
}

pub fn requests_with_canister_signature(env: TestEnv) {
let logger = env.logger();
let node = env.get_first_healthy_node_snapshot();
Expand Down
3 changes: 3 additions & 0 deletions rs/validator/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ rust_library(
"//rs/crypto/tree_hash",
"//rs/limits",
"//rs/types/types",
"@crate_index//:candid",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewer.

If you dislike adding candid as a dependency here, we can provide a non-candid endpoint in II to serve a different encoding of sender_info - please indicate your preferred encoding to keep this thread actionable.

"@crate_index//:hex",
"@crate_index//:serde",
"@crate_index//:serde_bytes",
"@crate_index//:thiserror",
],
)
Expand Down
4 changes: 3 additions & 1 deletion rs/validator/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ documentation.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
candid = { workspace = true }
hex = { workspace = true }
ic-limits = { path = "../limits" }
ic-crypto-iccsa = { path = "../crypto/iccsa" }
Expand All @@ -17,6 +18,8 @@ ic-crypto-sha2 = { path = "../crypto/sha2" }
ic-crypto-standalone-sig-verifier = { path = "../crypto/standalone-sig-verifier" }
ic-crypto-tree-hash = { path = "../crypto/tree_hash" }
ic-types = { path = "../types/types" }
serde = { workspace = true }
serde_bytes = { workspace = true }
thiserror = { workspace = true }

[dev-dependencies]
Expand All @@ -28,7 +31,6 @@ ic-crypto-test-utils-reproducible-rng = { path = "../crypto/test_utils/reproduci
ic-crypto-test-utils-root-of-trust = { path = "../crypto/test_utils/root_of_trust" }
ic-crypto-temp-crypto = { path = "../crypto/temp_crypto" }
ic-secp256r1 = { path = "../../packages/ic-secp256r1" }
serde = { workspace = true }
serde_cbor = { workspace = true }
serde_json = { workspace = true }
simple_asn1 = { workspace = true }
Expand Down
1 change: 1 addition & 0 deletions rs/validator/ingress_message/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ rust_test_suite(
"//rs/validator/http_request_test_utils",
"@crate_index//:assert_matches",
"@crate_index//:base64",
"@crate_index//:candid",
"@crate_index//:hex",
"@crate_index//:ic-cdk",
"@crate_index//:rand",
Expand Down
1 change: 1 addition & 0 deletions rs/validator/ingress_message/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ js = ["time/wasm-bindgen", "getrandom/js"]

[dev-dependencies]
assert_matches = { workspace = true }
candid = { workspace = true }
ic-canister-client-sender = { path = "../../canister_client/sender" }
ic-certification-test-utils = { path = "../../certification/test-utils" }
ic-limits = { path = "../../limits" }
Expand Down
3 changes: 3 additions & 0 deletions rs/validator/ingress_message/src/internal/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,9 @@ fn to_validation_error(error: ic_validator::RequestValidationError) -> RequestVa
ic_validator::RequestValidationError::SenderInfoRequiredByDelegation => {
RequestValidationError::SenderInfoRequiredByDelegation
}
ic_validator::RequestValidationError::UpdateCallNotPermittedBySenderInfo => {
RequestValidationError::UpdateCallNotPermittedBySenderInfo
}
}
}
fn to_authentication_lib_error(error: ic_validator::AuthenticationError) -> AuthenticationError {
Expand Down
10 changes: 10 additions & 0 deletions rs/validator/ingress_message/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub use internal::TimeProvider;
/// * [`RequestValidationError::InvalidSenderInfo`]: if sender info is provided but invalid.
/// * [`RequestValidationError::SenderInfoRequiredByDelegation`]: if the request is an update
/// call without a `sender_info` but a delegation requires one to be present.
/// * [`RequestValidationError::UpdateCallNotPermittedBySenderInfo`]: if the request is an update
/// call but the provided sender info only permits query calls.
pub trait HttpRequestVerifier<C> {
fn validate_request(&self, request: &HttpRequest<C>) -> Result<(), RequestValidationError>;
}
Expand All @@ -68,6 +70,7 @@ pub enum RequestValidationError {
NonceTooBigError { num_bytes: usize, maximum: usize },
InvalidSenderInfo(String),
SenderInfoRequiredByDelegation,
UpdateCallNotPermittedBySenderInfo,
}

impl Display for RequestValidationError {
Expand Down Expand Up @@ -122,6 +125,13 @@ impl Display for RequestValidationError {
but no sender_info was provided"
)
}
RequestValidationError::UpdateCallNotPermittedBySenderInfo => {
write!(
f,
"Sender info does not permit update calls: \
the \"implicit:permissions\" attribute is set to \"queries\""
)
}
}
}
}
Expand Down
Loading
Loading