diff --git a/.gitignore b/.gitignore index daaa0872..658ea216 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .DS_Store - +/.vscode/ /.idea/ # will have compiled files and executables diff --git a/ic-agent/src/agent/agent_error.rs b/ic-agent/src/agent/agent_error.rs index 93d17cac..1794fc53 100644 --- a/ic-agent/src/agent/agent_error.rs +++ b/ic-agent/src/agent/agent_error.rs @@ -116,6 +116,10 @@ pub enum AgentError { #[error("Certificate verification failed.")] CertificateVerificationFailed(), + /// The certificate contained a delegation that does not include the effective_canister_id in the canister_ranges field. + #[error("Certificate is not authorized to respond to queries for this canister. While developing: Did you forget to set effective_canister_id?")] + CertificateNotAuthorized(), + /// There was a length mismatch between the expected and actual length of the BLS DER-encoded public key. #[error( r#"BLS DER-encoded public key must be ${expected} bytes long, but is {actual} bytes long."# diff --git a/ic-agent/src/agent/agent_test.rs b/ic-agent/src/agent/agent_test.rs index 7f2ecaf3..32f96714 100644 --- a/ic-agent/src/agent/agent_test.rs +++ b/ic-agent/src/agent/agent_test.rs @@ -8,6 +8,7 @@ use crate::{ Status, }, export::Principal, + hash_tree::Label, Agent, AgentError, }; use mockito::mock; @@ -236,3 +237,221 @@ fn status_error() -> Result<(), AgentError> { Ok(()) } + +// these values for canister, paths, and mock_response are captured from a real request to mainnet +// the response amounts to "method not found" +// we don't really care about the response since we're just testing the cert verification +const REQ_WITH_DELEGATED_CERT_PATH: [&str; 2] = [ + "726571756573745F737461747573", + "92F03ABDDC774EE97882320CF15F2029A868FFCFE3BE48FEF84FC97B5A13E04A", +]; +const REQ_WITH_DELEGATED_CERT_CANISTER: &str = "ivg37-qiaaa-aaaab-aaaga-cai"; +const REQ_WITH_DELEGATED_CERT_RESPONSE: [u8; 1074] = [ + 217, 217, 247, 161, 107, 99, 101, 114, 116, 105, 102, 105, 99, 97, 116, 101, 89, 4, 31, 217, + 217, 247, 163, 100, 116, 114, 101, 101, 131, 1, 131, 1, 130, 4, 88, 32, 37, 15, 94, 38, 134, + 141, 156, 30, 167, 171, 41, 203, 233, 193, 91, 241, 196, 124, 13, 118, 5, 232, 3, 227, 158, 55, + 90, 127, 224, 156, 110, 187, 131, 1, 131, 2, 78, 114, 101, 113, 117, 101, 115, 116, 95, 115, + 116, 97, 116, 117, 115, 131, 1, 130, 4, 88, 32, 75, 38, 130, 39, 119, 78, 199, 127, 242, 179, + 126, 203, 18, 21, 115, 41, 213, 76, 243, 118, 105, 75, 221, 89, 222, 215, 128, 62, 253, 130, + 56, 111, 131, 2, 88, 32, 237, 173, 81, 14, 170, 160, 142, 210, 172, 212, 120, 19, 36, 230, 68, + 98, 105, 218, 103, 83, 236, 23, 118, 15, 32, 107, 190, 129, 196, 101, 255, 82, 131, 1, 131, 1, + 131, 2, 75, 114, 101, 106, 101, 99, 116, 95, 99, 111, 100, 101, 130, 3, 65, 3, 131, 2, 78, 114, + 101, 106, 101, 99, 116, 95, 109, 101, 115, 115, 97, 103, 101, 130, 3, 88, 68, 67, 97, 110, 105, + 115, 116, 101, 114, 32, 105, 118, 103, 51, 55, 45, 113, 105, 97, 97, 97, 45, 97, 97, 97, 97, + 98, 45, 97, 97, 97, 103, 97, 45, 99, 97, 105, 32, 104, 97, 115, 32, 110, 111, 32, 117, 112, + 100, 97, 116, 101, 32, 109, 101, 116, 104, 111, 100, 32, 39, 114, 101, 103, 105, 115, 116, 101, + 114, 39, 131, 2, 70, 115, 116, 97, 116, 117, 115, 130, 3, 72, 114, 101, 106, 101, 99, 116, 101, + 100, 130, 4, 88, 32, 151, 35, 47, 49, 246, 171, 124, 164, 254, 83, 235, 101, 104, 252, 62, 2, + 188, 34, 254, 148, 171, 49, 208, 16, 229, 251, 60, 100, 35, 1, 241, 96, 131, 1, 130, 4, 88, 32, + 58, 72, 209, 252, 33, 61, 73, 48, 113, 3, 16, 79, 125, 114, 194, 181, 147, 14, 219, 168, 120, + 123, 144, 99, 31, 52, 59, 58, 166, 138, 95, 10, 131, 2, 68, 116, 105, 109, 101, 130, 3, 73, + 226, 220, 147, 144, 145, 198, 150, 235, 22, 105, 115, 105, 103, 110, 97, 116, 117, 114, 101, + 88, 48, 137, 162, 190, 33, 181, 250, 138, 201, 250, 177, 82, 126, 4, 19, 39, 206, 137, 157, + 125, 169, 113, 67, 106, 31, 33, 101, 57, 57, 71, 180, 217, 66, 54, 91, 254, 84, 136, 113, 14, + 97, 166, 25, 186, 72, 56, 138, 33, 177, 106, 100, 101, 108, 101, 103, 97, 116, 105, 111, 110, + 162, 105, 115, 117, 98, 110, 101, 116, 95, 105, 100, 88, 29, 215, 123, 42, 47, 113, 153, 185, + 168, 174, 201, 63, 230, 251, 88, 134, 97, 53, 140, 241, 34, 35, 233, 163, 175, 123, 78, 186, + 196, 2, 107, 99, 101, 114, 116, 105, 102, 105, 99, 97, 116, 101, 89, 2, 49, 217, 217, 247, 162, + 100, 116, 114, 101, 101, 131, 1, 130, 4, 88, 32, 174, 2, 63, 40, 195, 185, 217, 102, 200, 251, + 9, 249, 237, 117, 92, 130, 138, 173, 181, 21, 46, 0, 170, 247, 0, 177, 140, 156, 6, 114, 148, + 180, 131, 1, 131, 2, 70, 115, 117, 98, 110, 101, 116, 131, 1, 130, 4, 88, 32, 232, 59, 176, 37, + 246, 87, 76, 143, 49, 35, 61, 192, 254, 40, 159, 245, 70, 223, 161, 228, 155, 214, 17, 109, + 214, 232, 137, 109, 144, 164, 148, 110, 131, 1, 130, 4, 88, 32, 231, 130, 97, 144, 146, 214, + 157, 91, 235, 240, 146, 65, 56, 189, 65, 22, 176, 21, 107, 90, 149, 226, 92, 53, 142, 168, 207, + 126, 113, 97, 166, 97, 131, 1, 131, 1, 130, 4, 88, 32, 98, 81, 63, 169, 38, 201, 169, 239, 128, + 58, 194, 132, 214, 32, 243, 3, 24, 149, 136, 225, 211, 144, 67, 73, 171, 99, 182, 71, 8, 86, + 252, 72, 131, 1, 130, 4, 88, 32, 96, 233, 163, 68, 206, 210, 201, 196, 169, 106, 1, 151, 253, + 88, 95, 45, 37, 157, 189, 25, 62, 78, 173, 165, 98, 57, 202, 194, 96, 135, 249, 197, 131, 2, + 88, 29, 215, 123, 42, 47, 113, 153, 185, 168, 174, 201, 63, 230, 251, 88, 134, 97, 53, 140, + 241, 34, 35, 233, 163, 175, 123, 78, 186, 196, 2, 131, 1, 131, 2, 79, 99, 97, 110, 105, 115, + 116, 101, 114, 95, 114, 97, 110, 103, 101, 115, 130, 3, 88, 27, 217, 217, 247, 129, 130, 74, 0, + 0, 0, 0, 0, 32, 0, 0, 1, 1, 74, 0, 0, 0, 0, 0, 47, 255, 255, 1, 1, 131, 2, 74, 112, 117, 98, + 108, 105, 99, 95, 107, 101, 121, 130, 3, 88, 133, 48, 129, 130, 48, 29, 6, 13, 43, 6, 1, 4, 1, + 130, 220, 124, 5, 3, 1, 2, 1, 6, 12, 43, 6, 1, 4, 1, 130, 220, 124, 5, 3, 2, 1, 3, 97, 0, 153, + 51, 225, 248, 158, 138, 60, 77, 127, 220, 204, 219, 213, 24, 8, 158, 43, 212, 216, 24, 10, 38, + 31, 24, 217, 194, 71, 165, 39, 104, 235, 206, 152, 220, 115, 40, 163, 152, 20, 168, 249, 17, 8, + 106, 29, 213, 12, 190, 1, 94, 42, 83, 183, 191, 120, 181, 82, 136, 137, 61, 170, 21, 195, 70, + 100, 14, 136, 49, 215, 42, 18, 189, 237, 217, 121, 210, 132, 112, 195, 72, 35, 184, 209, 195, + 244, 121, 93, 156, 57, 132, 162, 71, 19, 46, 148, 254, 130, 4, 88, 32, 153, 111, 23, 187, 146, + 107, 227, 49, 87, 69, 222, 167, 40, 32, 5, 167, 147, 181, 142, 118, 175, 235, 93, 67, 209, 162, + 140, 226, 157, 45, 21, 133, 131, 2, 68, 116, 105, 109, 101, 130, 3, 73, 149, 184, 170, 192, + 228, 237, 162, 234, 22, 105, 115, 105, 103, 110, 97, 116, 117, 114, 101, 88, 48, 172, 233, 252, + 221, 155, 201, 119, 224, 93, 99, 40, 248, 137, 220, 78, 124, 153, 17, 76, 115, 122, 73, 70, 83, + 203, 39, 161, 245, 92, 6, 244, 85, 94, 15, 22, 9, 128, 175, 94, 173, 9, 138, 204, 25, 80, 16, + 178, 247, +]; + +// this is the same response as REQ_WITH_DELEGATED_CERT_RESPONSE, but with a manually pruned +// /subnet//canister_ranges field +const PRUNED_SUBNET: [u8; 1064] = [ + 161, 107, 99, 101, 114, 116, 105, 102, 105, 99, 97, 116, 101, 89, 4, 24, 163, 100, 116, 114, + 101, 101, 131, 1, 131, 1, 130, 4, 88, 32, 37, 15, 94, 38, 134, 141, 156, 30, 167, 171, 41, 203, + 233, 193, 91, 241, 196, 124, 13, 118, 5, 232, 3, 227, 158, 55, 90, 127, 224, 156, 110, 187, + 131, 1, 131, 2, 78, 114, 101, 113, 117, 101, 115, 116, 95, 115, 116, 97, 116, 117, 115, 131, 1, + 130, 4, 88, 32, 75, 38, 130, 39, 119, 78, 199, 127, 242, 179, 126, 203, 18, 21, 115, 41, 213, + 76, 243, 118, 105, 75, 221, 89, 222, 215, 128, 62, 253, 130, 56, 111, 131, 2, 88, 32, 237, 173, + 81, 14, 170, 160, 142, 210, 172, 212, 120, 19, 36, 230, 68, 98, 105, 218, 103, 83, 236, 23, + 118, 15, 32, 107, 190, 129, 196, 101, 255, 82, 131, 1, 131, 1, 131, 2, 75, 114, 101, 106, 101, + 99, 116, 95, 99, 111, 100, 101, 130, 3, 65, 3, 131, 2, 78, 114, 101, 106, 101, 99, 116, 95, + 109, 101, 115, 115, 97, 103, 101, 130, 3, 88, 68, 67, 97, 110, 105, 115, 116, 101, 114, 32, + 105, 118, 103, 51, 55, 45, 113, 105, 97, 97, 97, 45, 97, 97, 97, 97, 98, 45, 97, 97, 97, 103, + 97, 45, 99, 97, 105, 32, 104, 97, 115, 32, 110, 111, 32, 117, 112, 100, 97, 116, 101, 32, 109, + 101, 116, 104, 111, 100, 32, 39, 114, 101, 103, 105, 115, 116, 101, 114, 39, 131, 2, 70, 115, + 116, 97, 116, 117, 115, 130, 3, 72, 114, 101, 106, 101, 99, 116, 101, 100, 130, 4, 88, 32, 151, + 35, 47, 49, 246, 171, 124, 164, 254, 83, 235, 101, 104, 252, 62, 2, 188, 34, 254, 148, 171, 49, + 208, 16, 229, 251, 60, 100, 35, 1, 241, 96, 131, 1, 130, 4, 88, 32, 58, 72, 209, 252, 33, 61, + 73, 48, 113, 3, 16, 79, 125, 114, 194, 181, 147, 14, 219, 168, 120, 123, 144, 99, 31, 52, 59, + 58, 166, 138, 95, 10, 131, 2, 68, 116, 105, 109, 101, 130, 3, 73, 226, 220, 147, 144, 145, 198, + 150, 235, 22, 105, 115, 105, 103, 110, 97, 116, 117, 114, 101, 88, 48, 137, 162, 190, 33, 181, + 250, 138, 201, 250, 177, 82, 126, 4, 19, 39, 206, 137, 157, 125, 169, 113, 67, 106, 31, 33, + 101, 57, 57, 71, 180, 217, 66, 54, 91, 254, 84, 136, 113, 14, 97, 166, 25, 186, 72, 56, 138, + 33, 177, 106, 100, 101, 108, 101, 103, 97, 116, 105, 111, 110, 162, 105, 115, 117, 98, 110, + 101, 116, 95, 105, 100, 88, 29, 215, 123, 42, 47, 113, 153, 185, 168, 174, 201, 63, 230, 251, + 88, 134, 97, 53, 140, 241, 34, 35, 233, 163, 175, 123, 78, 186, 196, 2, 107, 99, 101, 114, 116, + 105, 102, 105, 99, 97, 116, 101, 89, 2, 45, 163, 100, 116, 114, 101, 101, 131, 1, 130, 4, 88, + 32, 174, 2, 63, 40, 195, 185, 217, 102, 200, 251, 9, 249, 237, 117, 92, 130, 138, 173, 181, 21, + 46, 0, 170, 247, 0, 177, 140, 156, 6, 114, 148, 180, 131, 1, 131, 2, 70, 115, 117, 98, 110, + 101, 116, 131, 1, 130, 4, 88, 32, 232, 59, 176, 37, 246, 87, 76, 143, 49, 35, 61, 192, 254, 40, + 159, 245, 70, 223, 161, 228, 155, 214, 17, 109, 214, 232, 137, 109, 144, 164, 148, 110, 131, 1, + 130, 4, 88, 32, 231, 130, 97, 144, 146, 214, 157, 91, 235, 240, 146, 65, 56, 189, 65, 22, 176, + 21, 107, 90, 149, 226, 92, 53, 142, 168, 207, 126, 113, 97, 166, 97, 131, 1, 131, 1, 130, 4, + 88, 32, 98, 81, 63, 169, 38, 201, 169, 239, 128, 58, 194, 132, 214, 32, 243, 3, 24, 149, 136, + 225, 211, 144, 67, 73, 171, 99, 182, 71, 8, 86, 252, 72, 131, 1, 130, 4, 88, 32, 96, 233, 163, + 68, 206, 210, 201, 196, 169, 106, 1, 151, 253, 88, 95, 45, 37, 157, 189, 25, 62, 78, 173, 165, + 98, 57, 202, 194, 96, 135, 249, 197, 131, 2, 88, 29, 215, 123, 42, 47, 113, 153, 185, 168, 174, + 201, 63, 230, 251, 88, 134, 97, 53, 140, 241, 34, 35, 233, 163, 175, 123, 78, 186, 196, 2, 131, + 1, 130, 4, 88, 32, 32, 38, 201, 161, 171, 93, 204, 127, 80, 161, 230, 124, 235, 148, 89, 31, 6, + 180, 77, 141, 245, 169, 134, 51, 104, 168, 66, 91, 121, 228, 125, 38, 131, 2, 74, 112, 117, 98, + 108, 105, 99, 95, 107, 101, 121, 130, 3, 88, 133, 48, 129, 130, 48, 29, 6, 13, 43, 6, 1, 4, 1, + 130, 220, 124, 5, 3, 1, 2, 1, 6, 12, 43, 6, 1, 4, 1, 130, 220, 124, 5, 3, 2, 1, 3, 97, 0, 153, + 51, 225, 248, 158, 138, 60, 77, 127, 220, 204, 219, 213, 24, 8, 158, 43, 212, 216, 24, 10, 38, + 31, 24, 217, 194, 71, 165, 39, 104, 235, 206, 152, 220, 115, 40, 163, 152, 20, 168, 249, 17, 8, + 106, 29, 213, 12, 190, 1, 94, 42, 83, 183, 191, 120, 181, 82, 136, 137, 61, 170, 21, 195, 70, + 100, 14, 136, 49, 215, 42, 18, 189, 237, 217, 121, 210, 132, 112, 195, 72, 35, 184, 209, 195, + 244, 121, 93, 156, 57, 132, 162, 71, 19, 46, 148, 254, 130, 4, 88, 32, 153, 111, 23, 187, 146, + 107, 227, 49, 87, 69, 222, 167, 40, 32, 5, 167, 147, 181, 142, 118, 175, 235, 93, 67, 209, 162, + 140, 226, 157, 45, 21, 133, 131, 2, 68, 116, 105, 109, 101, 130, 3, 73, 149, 184, 170, 192, + 228, 237, 162, 234, 22, 105, 115, 105, 103, 110, 97, 116, 117, 114, 101, 88, 48, 172, 233, 252, + 221, 155, 201, 119, 224, 93, 99, 40, 248, 137, 220, 78, 124, 153, 17, 76, 115, 122, 73, 70, 83, + 203, 39, 161, 245, 92, 6, 244, 85, 94, 15, 22, 9, 128, 175, 94, 173, 9, 138, 204, 25, 80, 16, + 178, 247, 106, 100, 101, 108, 101, 103, 97, 116, 105, 111, 110, 246, +]; + +#[test] +// asserts that a delegated certificate with correct /subnet//canister_ranges +// passes the certificate verification +fn check_subnet_range_with_valid_range() { + let _read_mock = mock( + "POST", + "/api/v2/canister/ivg37-qiaaa-aaaab-aaaga-cai/read_state", + ) + .with_status(200) + .with_body(&REQ_WITH_DELEGATED_CERT_RESPONSE) + .create(); + let agent = Agent::builder() + .with_transport(ReqwestHttpReplicaV2Transport::create(&mockito::server_url()).unwrap()) + .build() + .unwrap(); + let runtime = tokio::runtime::Runtime::new().expect("Unable to create a runtime"); + let _result = runtime + .block_on(async { + agent + .read_state_raw( + vec![REQ_WITH_DELEGATED_CERT_PATH + .iter() + .map(Label::from) + .collect()], + Principal::from_text(REQ_WITH_DELEGATED_CERT_CANISTER).unwrap(), + false, + ) + .await + }) + .expect("read state failed"); +} + +#[test] +// asserts that a delegated certificate with /subnet//canister_ranges that don't include +// the canister gets rejected by the cert verification because the subnet is not authorized to +// respond to requests for this canister. We do this by using a correct response but serving it +// for the wrong canister, which a malicious node might do. +fn check_subnet_range_with_unauthorized_range() { + let wrong_canister = Principal::from_text("ryjl3-tyaaa-aaaaa-aaaba-cai").unwrap(); + let _read_mock = mock( + "POST", + "/api/v2/canister/ryjl3-tyaaa-aaaaa-aaaba-cai/read_state", + ) + .with_status(200) + .with_body(&REQ_WITH_DELEGATED_CERT_RESPONSE) + .create(); + let agent = Agent::builder() + .with_transport(ReqwestHttpReplicaV2Transport::create(&mockito::server_url()).unwrap()) + .build() + .unwrap(); + let runtime = tokio::runtime::Runtime::new().expect("Unable to create a runtime"); + let result = runtime.block_on(async { + agent + .read_state_raw( + vec![REQ_WITH_DELEGATED_CERT_PATH + .iter() + .map(Label::from) + .collect()], + wrong_canister, + false, + ) + .await + }); + assert_eq!(result, Err(AgentError::CertificateNotAuthorized())); +} + +#[test] +// asserts that a delegated certificate with pruned/removed /subnet//canister_ranges +// gets rejected by the cert verification. We do this by using a correct response that has +// the leaf manually pruned +fn check_subnet_range_with_pruned_range() { + let canister = Principal::from_text("ivg37-qiaaa-aaaab-aaaga-cai").unwrap(); + let _read_mock = mock( + "POST", + "/api/v2/canister/ivg37-qiaaa-aaaab-aaaga-cai/read_state", + ) + .with_status(200) + .with_body(&PRUNED_SUBNET) + .create(); + let agent = Agent::builder() + .with_transport(ReqwestHttpReplicaV2Transport::create(&mockito::server_url()).unwrap()) + .build() + .unwrap(); + let runtime = tokio::runtime::Runtime::new().expect("Unable to create a runtime"); + let result = runtime.block_on(async { + agent + .read_state_raw( + vec![REQ_WITH_DELEGATED_CERT_PATH + .iter() + .map(Label::from) + .collect()], + canister, + false, + ) + .await + }); + assert!(result.is_err()); +} diff --git a/ic-agent/src/agent/mod.rs b/ic-agent/src/agent/mod.rs index 87556396..8903f46b 100644 --- a/ic-agent/src/agent/mod.rs +++ b/ic-agent/src/agent/mod.rs @@ -249,7 +249,7 @@ pub enum PollResult { /// ``` /// /// This agent does not understand Candid, and only acts on byte buffers. -#[derive(Clone, Debug)] +#[derive(Clone)] pub struct Agent { nonce_factory: Arc, identity: Arc, @@ -258,6 +258,14 @@ pub struct Agent { transport: Arc, } +impl fmt::Debug for Agent { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + f.debug_struct("Agent") + .field("ingress_expiry_duration", &self.ingress_expiry_duration) + .finish_non_exhaustive() + } +} + impl Agent { /// Create an instance of an [`AgentBuilder`] for building an [`Agent`]. This is simpler than /// using the [`AgentConfig`] and [`Agent::new()`]. @@ -516,10 +524,11 @@ impl Agent { pub async fn poll( &self, request_id: &RequestId, - effective_canister_id: &Principal, + effective_canister_id: Principal, + disable_range_check: bool, ) -> Result { match self - .request_status_raw(request_id, *effective_canister_id) + .request_status_raw(request_id, effective_canister_id, disable_range_check) .await? { RequestStatusResponse::Unknown => Ok(PollResult::Submitted), @@ -549,13 +558,17 @@ impl Agent { pub async fn wait( &self, request_id: RequestId, - effective_canister_id: &Principal, + effective_canister_id: Principal, + disable_range_check: bool, mut waiter: W, ) -> Result, AgentError> { waiter.start(); let mut request_accepted = false; loop { - match self.poll(&request_id, effective_canister_id).await? { + match self + .poll(&request_id, effective_canister_id, disable_range_check) + .await? + { PollResult::Submitted => {} PollResult::Accepted => { if !request_accepted { @@ -587,6 +600,7 @@ impl Agent { &self, paths: Vec>, effective_canister_id: Principal, + disable_range_check: bool, ) -> Result, AgentError> { let request = self.read_state_content(paths)?; let serialized_bytes = sign_request(&request, self.identity.clone())?; @@ -594,10 +608,9 @@ impl Agent { let read_state_response: ReadStateResponse = self .read_state_endpoint(effective_canister_id, serialized_bytes) .await?; - let cert: Certificate = serde_cbor::from_slice(&read_state_response.certificate) .map_err(AgentError::InvalidCborData)?; - self.verify(&cert)?; + self.verify(&cert, effective_canister_id, disable_range_check)?; Ok(cert) } @@ -610,7 +623,13 @@ impl Agent { } /// Verify a certificate, checking delegation if present. - pub fn verify(&self, cert: &Certificate) -> Result<(), AgentError> { + /// Only passes if the certificate also has authority over the canister. + pub fn verify( + &self, + cert: &Certificate, + effective_canister_id: Principal, + disable_range_check: bool, + ) -> Result<(), AgentError> { let sig = &cert.signature; let root_hash = cert.tree.digest(); @@ -618,9 +637,9 @@ impl Agent { msg.extend_from_slice(IC_STATE_ROOT_DOMAIN_SEPARATOR); msg.extend_from_slice(&root_hash); - let der_key = self.check_delegation(&cert.delegation)?; + let der_key = + self.check_delegation(&cert.delegation, effective_canister_id, disable_range_check)?; let key = extract_der(der_key)?; - let result = bls::core_verify(sig, &*msg, &*key); if result != bls::BLS_OK { Err(AgentError::CertificateVerificationFailed()) @@ -629,13 +648,33 @@ impl Agent { } } - fn check_delegation(&self, delegation: &Option) -> Result, AgentError> { + fn check_delegation( + &self, + delegation: &Option, + effective_canister_id: Principal, + disable_range_check: bool, + ) -> Result, AgentError> { match delegation { None => self.read_root_key(), Some(delegation) => { let cert: Certificate = serde_cbor::from_slice(&delegation.certificate) .map_err(AgentError::InvalidCborData)?; - self.verify(&cert)?; + self.verify(&cert, effective_canister_id, disable_range_check)?; + let canister_range_lookup = [ + "subnet".into(), + delegation.subnet_id.clone().into(), + "canister_ranges".into(), + ]; + let canister_range = lookup_value(&cert, canister_range_lookup)?; + let ranges: Vec<(Principal, Principal)> = + serde_cbor::from_slice(canister_range).map_err(AgentError::InvalidCborData)?; + if !disable_range_check + && !principal_is_within_ranges(&effective_canister_id, &ranges[..]) + { + // the certificate is not authorized to answer calls for this canister + return Err(AgentError::CertificateNotAuthorized()); + } + let public_key_path = [ "subnet".into(), delegation.subnet_id.clone().into(), @@ -651,10 +690,13 @@ impl Agent { &self, canister_id: Principal, path: &str, + disable_range_check: bool, ) -> Result, AgentError> { let paths: Vec> = vec![vec!["canister".into(), canister_id.into(), path.into()]]; - let cert = self.read_state_raw(paths, canister_id).await?; + let cert = self + .read_state_raw(paths, canister_id, disable_range_check) + .await?; lookup_canister_info(cert, canister_id, path) } @@ -664,6 +706,7 @@ impl Agent { &self, canister_id: Principal, path: &str, + disable_range_check: bool, ) -> Result, AgentError> { let paths: Vec> = vec![vec![ "canister".into(), @@ -672,7 +715,9 @@ impl Agent { path.into(), ]]; - let cert = self.read_state_raw(paths, canister_id).await?; + let cert = self + .read_state_raw(paths, canister_id, disable_range_check) + .await?; lookup_canister_metadata(cert, canister_id, path) } @@ -682,11 +727,14 @@ impl Agent { &self, request_id: &RequestId, effective_canister_id: Principal, + disable_range_check: bool, ) -> Result { let paths: Vec> = vec![vec!["request_status".into(), request_id.to_vec().into()]]; - let cert = self.read_state_raw(paths, effective_canister_id).await?; + let cert = self + .read_state_raw(paths, effective_canister_id, disable_range_check) + .await?; lookup_request_status(cert, request_id) } @@ -699,6 +747,7 @@ impl Agent { request_id: &RequestId, effective_canister_id: Principal, signed_request_status: Vec, + disable_range_check: bool, ) -> Result { let _envelope: Envelope = serde_cbor::from_slice(&signed_request_status).map_err(AgentError::InvalidCborData)?; @@ -708,7 +757,7 @@ impl Agent { let cert: Certificate = serde_cbor::from_slice(&read_state_response.certificate) .map_err(AgentError::InvalidCborData)?; - self.verify(&cert)?; + self.verify(&cert, effective_canister_id, disable_range_check)?; lookup_request_status(cert, request_id) } @@ -765,6 +814,14 @@ impl Agent { } } +// Checks if a principal is contained within a list of principal ranges +// A range is a tuple: (low: Principal, high: Principal), as described here: https://docs.dfinity.systems/spec/public/#state-tree-subnet +fn principal_is_within_ranges(principal: &Principal, ranges: &[(Principal, Principal)]) -> bool { + ranges + .iter() + .any(|r| principal >= &r.0 && principal <= &r.1) +} + fn construct_message(request_id: &RequestId) -> Vec { let mut buf = vec![]; buf.extend_from_slice(IC_REQUEST_DOMAIN_SEPARATOR); @@ -1083,6 +1140,7 @@ pub struct UpdateCall<'agent> { agent: &'agent Agent, request_id: Pin> + Send + 'agent>>, effective_canister_id: Principal, + disable_range_check: bool, } impl fmt::Debug for UpdateCall<'_> { @@ -1116,7 +1174,12 @@ impl UpdateCall<'_> { let request_id = _self.request_id.await?; _self .agent - .wait(request_id, &_self.effective_canister_id, waiter) + .wait( + request_id, + _self.effective_canister_id, + _self.disable_range_check, + waiter, + ) .await } Box::pin(run(self, waiter)) @@ -1139,11 +1202,16 @@ pub struct UpdateBuilder<'agent> { pub arg: Vec, /// The Unix timestamp that the request will expire at. pub ingress_expiry_datetime: Option, + disable_range_check: bool, } impl<'agent> UpdateBuilder<'agent> { /// Creates a new query builder with an agent for a particular canister method. pub fn new(agent: &'agent Agent, canister_id: Principal, method_name: String) -> Self { + // When calling provisional_create_canister_with_cycles, every effective_canister_id is valid. + // Therefore we need to disable the check for valid canister_ranges in the certificate validation. + // More info: https://docs.dfinity.systems/spec/public/#http-effective-canister-id + let disable_range_check = method_name == "provisional_create_canister_with_cycles"; Self { agent, effective_canister_id: canister_id, @@ -1151,6 +1219,7 @@ impl<'agent> UpdateBuilder<'agent> { method_name, arg: vec![], ingress_expiry_datetime: None, + disable_range_check, } } @@ -1218,6 +1287,7 @@ impl<'agent> UpdateBuilder<'agent> { agent: self.agent, request_id: Box::pin(request_id_future), effective_canister_id: self.effective_canister_id, + disable_range_check: self.disable_range_check, } } diff --git a/ic-agent/src/agent/replica_api.rs b/ic-agent/src/agent/replica_api.rs index 992b1360..8732fc1a 100644 --- a/ic-agent/src/agent/replica_api.rs +++ b/ic-agent/src/agent/replica_api.rs @@ -109,7 +109,7 @@ pub struct ReadStateResponse { } /// A `Certificate` as defined in https://smartcontracts.org/docs/interface-spec/index.html#_certificate -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, PartialEq, Eq)] pub struct Certificate<'a> { /// The hash tree. pub tree: HashTree<'a>, @@ -122,7 +122,7 @@ pub struct Certificate<'a> { pub delegation: Option, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, PartialEq, Eq)] pub struct Delegation { #[serde(with = "serde_bytes")] pub subnet_id: Vec, diff --git a/ic-asset/src/asset_canister/chunk.rs b/ic-asset/src/asset_canister/chunk.rs index d0cb5668..4db2652e 100644 --- a/ic-asset/src/asset_canister/chunk.rs +++ b/ic-asset/src/asset_canister/chunk.rs @@ -46,6 +46,7 @@ pub(crate) async fn create_chunk( .wait( request_id, waiter_with_timeout(canister_call_params.timeout), + false, ) .await } diff --git a/ic-utils/src/canister.rs b/ic-utils/src/canister.rs index 8cfafc87..b802d15a 100644 --- a/ic-utils/src/canister.rs +++ b/ic-utils/src/canister.rs @@ -147,11 +147,14 @@ impl<'agent, T> Canister<'agent, T> { &'canister self, request_id: RequestId, waiter: W, + disable_range_check: bool, ) -> Result, AgentError> where W: Waiter, { - self.agent.wait(request_id, &self.canister_id, waiter).await + self.agent + .wait(request_id, self.canister_id, disable_range_check, waiter) + .await } } diff --git a/icx/src/main.rs b/icx/src/main.rs index 31cbb961..a30d4778 100644 --- a/icx/src/main.rs +++ b/icx/src/main.rs @@ -558,6 +558,7 @@ async fn main() -> Result<()> { &signed_request_status.request_id, signed_request_status.effective_canister_id, signed_request_status.signed_request_status, + false, ) .await .context("Got an error when send the signed request_status call")?; diff --git a/ref-tests/tests/integration.rs b/ref-tests/tests/integration.rs index d39f8e6c..3e3db6b7 100644 --- a/ref-tests/tests/integration.rs +++ b/ref-tests/tests/integration.rs @@ -560,6 +560,7 @@ mod sign_send { &signed_request_status.request_id, signed_request_status.effective_canister_id, signed_request_status.signed_request_status.clone(), + false, ) .await?;