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
28 changes: 28 additions & 0 deletions rs/types/types/src/messages/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,7 @@ pub struct Delegation {
pubkey: Blob,
expiration: Time,
targets: Option<Vec<Blob>>,
sender_info_required: Option<bool>,
}

impl Delegation {
Expand All @@ -552,6 +553,7 @@ impl Delegation {
pubkey: Blob(pubkey),
expiration,
targets: None,
sender_info_required: None,
}
}

Expand All @@ -560,6 +562,20 @@ impl Delegation {
pubkey: Blob(pubkey),
expiration,
targets: Some(targets.iter().map(|c| Blob(c.get().to_vec())).collect()),
sender_info_required: None,
}
}

pub fn new_with_sender_info_required(
pubkey: Vec<u8>,
expiration: Time,
sender_info_required: bool,
) -> Self {
Self {
pubkey: Blob(pubkey),
expiration,
targets: None,
sender_info_required: Some(sender_info_required),
}
}

Expand Down Expand Up @@ -590,6 +606,12 @@ impl Delegation {
pub fn number_of_targets(&self) -> Option<usize> {
self.targets.as_ref().map(Vec::len)
}

/// Whether the delegation requires requests authenticated through it to
/// carry a signed `sender_info`.
pub fn sender_info_required(&self) -> bool {
self.sender_info_required.unwrap_or(false)
}
}

impl SignedBytesWithoutDomainSeparator for Delegation {
Expand All @@ -606,6 +628,12 @@ impl SignedBytesWithoutDomainSeparator for Delegation {
Array(targets.iter().map(|t| Bytes(t.0.as_slice())).collect()),
);
}
if let Some(sender_info_required) = self.sender_info_required {
map.insert(
"sender_info_required",
U64(if sender_info_required { 1 } else { 0 }),
);
}

bytes.extend_from_slice(&hash_of_map(&map, |key, value| hash_key_val(key, value)));
}
Expand Down
23 changes: 23 additions & 0 deletions rs/types/types/src/messages/http/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ mod targets {
invalid_canister_id,
to_blob(CanisterId::from(3)),
]),
sender_info_required: None,
};

let targets = delegation.targets();
Expand All @@ -33,6 +34,7 @@ mod targets {
let delegation = Delegation {
pubkey: Blob(vec![]),
expiration: CURRENT_TIME,
sender_info_required: None,
targets: Some(vec![
to_blob(canister_id_3),
to_blob(canister_id_3),
Expand Down Expand Up @@ -914,11 +916,13 @@ mod cbor_serialization {
pubkey: Blob(vec![1, 2, 3]),
expiration: UNIX_EPOCH,
targets: None,
sender_info_required: None,
},
Value::Map(btreemap! {
text("pubkey") => bytes(&[1, 2, 3]),
text("expiration") => int(0),
text("targets") => Value::Null,
text("sender_info_required") => Value::Null,
}),
);

Expand All @@ -927,11 +931,28 @@ mod cbor_serialization {
pubkey: Blob(vec![1, 2, 3]),
expiration: UNIX_EPOCH,
targets: Some(vec![Blob(vec![4, 5, 6])]),
sender_info_required: None,
},
Value::Map(btreemap! {
text("pubkey") => bytes(&[1, 2, 3]),
text("expiration") => int(0),
text("targets") => Value::Array(vec![bytes(&[4, 5, 6])]),
text("sender_info_required") => Value::Null,
}),
);

assert_cbor_ser_equal(
&Delegation {
pubkey: Blob(vec![1, 2, 3]),
expiration: UNIX_EPOCH,
targets: None,
sender_info_required: Some(true),
},
Value::Map(btreemap! {
text("pubkey") => bytes(&[1, 2, 3]),
text("expiration") => int(0),
text("targets") => Value::Null,
text("sender_info_required") => Value::Bool(true),
}),
);
}
Expand All @@ -944,6 +965,7 @@ mod cbor_serialization {
pubkey: Blob(vec![1, 2, 3]),
expiration: UNIX_EPOCH,
targets: None,
sender_info_required: None,
},
signature: Blob(vec![4, 5, 6]),
},
Expand All @@ -952,6 +974,7 @@ mod cbor_serialization {
text("pubkey") => bytes(&[1, 2, 3]),
text("expiration") => int(0),
text("targets") => Value::Null,
text("sender_info_required") => Value::Null,
}),
text("signature") => bytes(&[4, 5, 6]),
}),
Expand Down
33 changes: 33 additions & 0 deletions rs/validator/http_request_test_utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,22 @@ impl DirectAuthenticationScheme {
let signature = self.sign(&delegation);
SignedDelegation::new(delegation, signature)
}

/// Creates a delegation that requires calls to carry a signed `sender_info`.
fn delegate_to_with_sender_info_required(
&self,
other: &DirectAuthenticationScheme,
expiration: Time,
sender_info_required: bool,
) -> SignedDelegation {
let delegation = Delegation::new_with_sender_info_required(
other.public_key_der(),
expiration,
sender_info_required,
);
let signature = self.sign(&delegation);
SignedDelegation::new(delegation, signature)
}
}

#[derive(Clone, Eq, PartialEq, Debug)]
Expand Down Expand Up @@ -595,6 +611,23 @@ impl DelegationChainBuilder {
self
}

pub fn delegate_to_with_sender_info_required(
mut self,
new_end: DirectAuthenticationScheme,
expiration: Time,
sender_info_required: bool,
) -> Self {
let current_end = self.end.unwrap_or_else(|| self.start.clone());
self.signed_delegations
.push(current_end.delegate_to_with_sender_info_required(
&new_end,
expiration,
sender_info_required,
));
self.end = Some(new_end);
self
}

pub fn change_last_delegation<F: FnOnce(SignedDelegationBuilder) -> SignedDelegationBuilder>(
mut self,
change: F,
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 @@ -208,6 +208,9 @@ fn to_validation_error(error: ic_validator::RequestValidationError) -> RequestVa
ic_validator::RequestValidationError::InvalidSenderInfo(msg) => {
RequestValidationError::InvalidSenderInfo(msg)
}
ic_validator::RequestValidationError::SenderInfoRequiredByDelegation => {
RequestValidationError::SenderInfoRequiredByDelegation
}
}
}
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 @@ -46,6 +46,8 @@ pub use internal::TimeProvider;
/// * [`RequestValidationError::CanisterNotInDelegationTargets`]: if the request targets a canister
/// that is not authorized in one of the delegations.
/// * [`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.
pub trait HttpRequestVerifier<C> {
fn validate_request(&self, request: &HttpRequest<C>) -> Result<(), RequestValidationError>;
}
Expand All @@ -65,6 +67,7 @@ pub enum RequestValidationError {
PathTooLongError { length: usize, maximum: usize },
NonceTooBigError { num_bytes: usize, maximum: usize },
InvalidSenderInfo(String),
SenderInfoRequiredByDelegation,
}

impl Display for RequestValidationError {
Expand Down Expand Up @@ -112,6 +115,13 @@ impl Display for RequestValidationError {
RequestValidationError::InvalidSenderInfo(msg) => {
write!(f, "Invalid sender info: {msg}")
}
RequestValidationError::SenderInfoRequiredByDelegation => {
write!(
f,
"A delegation requires the call to carry a signed sender info, \
but no sender_info was provided"
)
}
}
}
}
Expand Down
135 changes: 135 additions & 0 deletions rs/validator/ingress_message/tests/validate_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2175,6 +2175,141 @@ mod sender_info {
}
}

mod delegation_sender_info_required {
use super::*;
use ic_types::messages::{RawSignedSenderInfo, SenderInfoContent};
use ic_validator_http_request_test_utils::DelegationChain;
use ic_validator_http_request_test_utils::DirectAuthenticationScheme::CanisterSignature;

/// Creates a canister signer (acting as the delegation issuer, e.g.
/// Internet Identity), a delegation chain rooted at it that requires a
/// signed `sender_info`, and a valid `RawSignedSenderInfo` signed by the
/// same canister.
fn setup<R: Rng + CryptoRng>(
rng: &mut R,
sender_info_required: bool,
) -> (
IngressMessageVerifierBuilder,
DelegationChain,
RawSignedSenderInfo,
) {
let root_of_trust = RootOfTrust::new_random(rng);
let builder = default_verifier().with_root_of_trust(root_of_trust.public_key);
let signer = CanisterSigner {
seed: CANISTER_SIGNATURE_SEED.to_vec(),
canister_id: CANISTER_ID_SIGNER,
root_public_key: root_of_trust.public_key,
root_secret_key: root_of_trust.secret_key,
};
let info_bytes = b"certified session attributes".to_vec();
let sig = signer.sign(&SenderInfoContent(&info_bytes));
let sender_info = RawSignedSenderInfo {
info: Blob(info_bytes),
signer: Blob(CANISTER_ID_SIGNER.get().into_vec()),
sig: Blob(sig.0),
};
let chain = DelegationChain::rooted_at(CanisterSignature(signer))
.delegate_to_with_sender_info_required(
random_user_key_pair(rng),
CURRENT_TIME,
sender_info_required,
)
.build();
(builder, chain, sender_info)
}

#[test]
fn should_reject_update_call_without_sender_info_when_delegation_requires_it() {
let rng = &mut reproducible_rng();
let (builder, chain, _sender_info) = setup(rng, true);
let verifier = builder.build();

let request = HttpRequestBuilder::new_update_call()
.with_ingress_expiry_at(CURRENT_TIME)
.with_authentication(AuthenticationScheme::Delegation(chain))
.build();

assert_matches!(
verifier.validate_request(&request),
Err(RequestValidationError::SenderInfoRequiredByDelegation)
);
}

#[test]
fn should_accept_update_call_with_sender_info_when_delegation_requires_it() {
let rng = &mut reproducible_rng();
let (builder, chain, sender_info) = setup(rng, true);
let verifier = builder.build();

let request = HttpRequestBuilder::new_update_call()
.with_ingress_expiry_at(CURRENT_TIME)
.with_authentication(AuthenticationScheme::Delegation(chain))
.with_sender_info(sender_info)
.build();

assert_eq!(verifier.validate_request(&request), Ok(()));
}

#[test]
fn should_accept_query_without_sender_info_when_delegation_requires_it() {
let rng = &mut reproducible_rng();
let (builder, chain, _sender_info) = setup(rng, true);
let verifier = builder.build();

let request = HttpRequestBuilder::new_query()
.with_ingress_expiry_at(CURRENT_TIME)
.with_authentication(AuthenticationScheme::Delegation(chain))
.build();

assert_eq!(verifier.validate_request(&request), Ok(()));
}

#[test]
fn should_accept_update_call_without_sender_info_when_not_required() {
let rng = &mut reproducible_rng();
let (builder, chain, _sender_info) = setup(rng, false);
let verifier = builder.build();

let request = HttpRequestBuilder::new_update_call()
.with_ingress_expiry_at(CURRENT_TIME)
.with_authentication(AuthenticationScheme::Delegation(chain))
.build();

assert_eq!(verifier.validate_request(&request), Ok(()));
}

#[test]
fn should_reject_update_call_when_any_delegation_in_chain_requires_sender_info() {
let rng = &mut reproducible_rng();
let root_of_trust = RootOfTrust::new_random(rng);
let verifier = default_verifier()
.with_root_of_trust(root_of_trust.public_key)
.build();
let signer = CanisterSigner {
seed: CANISTER_SIGNATURE_SEED.to_vec(),
canister_id: CANISTER_ID_SIGNER,
root_public_key: root_of_trust.public_key,
root_secret_key: root_of_trust.secret_key,
};
// The requirement sits in the middle of the chain; subsequent
// delegations must not lift it.
let chain = DelegationChain::rooted_at(CanisterSignature(signer))
.delegate_to_with_sender_info_required(random_user_key_pair(rng), CURRENT_TIME, true)
.delegate_to(random_user_key_pair(rng), CURRENT_TIME)
.build();

let request = HttpRequestBuilder::new_update_call()
.with_ingress_expiry_at(CURRENT_TIME)
.with_authentication(AuthenticationScheme::Delegation(chain))
.build();

assert_matches!(
verifier.validate_request(&request),
Err(RequestValidationError::SenderInfoRequiredByDelegation)
);
}
}

fn default_verifier() -> IngressMessageVerifierBuilder {
IngressMessageVerifier::builder().with_time_provider(TimeProvider::Constant(CURRENT_TIME))
}
Expand Down
Loading
Loading