diff --git a/CHANGELOG.md b/CHANGELOG.md index 3471357f..fdacc50d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +* Added `DelegatedIdentity`, an `Identity` implementation for consuming delegations such as those from Internet Identity. * Replica protocol type definitions have been moved to an `ic-transport-types` crate. `ic-agent` still reexports the ones for its API. * The `Unknown` lookup of a request_status path in a certificate results in an `AgentError` (the IC returns `Absent` for non-existing paths). * For `Canister` type, added methods with no trailing underscore: update(), query(), canister_id(), clone_with() diff --git a/ic-agent/src/agent/mod.rs b/ic-agent/src/agent/mod.rs index 3ce0af7e..1cff7809 100644 --- a/ic-agent/src/agent/mod.rs +++ b/ic-agent/src/agent/mod.rs @@ -799,7 +799,7 @@ fn sign_envelope( content: Cow::Borrowed(content), sender_pubkey: signature.public_key, sender_sig: signature.signature, - sender_delegation: None, + sender_delegation: signature.delegations, }; let mut serialized_bytes = Vec::new(); diff --git a/ic-agent/src/identity/anonymous.rs b/ic-agent/src/identity/anonymous.rs index d0631feb..4df17394 100644 --- a/ic-agent/src/identity/anonymous.rs +++ b/ic-agent/src/identity/anonymous.rs @@ -19,6 +19,7 @@ impl Identity for AnonymousIdentity { Ok(Signature { signature: None, public_key: None, + delegations: None, }) } @@ -26,6 +27,7 @@ impl Identity for AnonymousIdentity { Ok(Signature { public_key: None, signature: None, + delegations: None, }) } } diff --git a/ic-agent/src/identity/basic.rs b/ic-agent/src/identity/basic.rs index 008005a9..d88b9165 100644 --- a/ic-agent/src/identity/basic.rs +++ b/ic-agent/src/identity/basic.rs @@ -79,6 +79,7 @@ impl Identity for BasicIdentity { Ok(Signature { signature: Some(signature.as_ref().to_vec()), public_key: self.public_key(), + delegations: None, }) } } diff --git a/ic-agent/src/identity/delegated.rs b/ic-agent/src/identity/delegated.rs new file mode 100644 index 00000000..623b03c8 --- /dev/null +++ b/ic-agent/src/identity/delegated.rs @@ -0,0 +1,60 @@ +use candid::Principal; + +use crate::{agent::EnvelopeContent, Signature}; + +use super::{Delegation, Identity, SignedDelegation}; + +/// An identity that has been delegated the authority to authenticate as a different principal. +pub struct DelegatedIdentity { + to: Box, + chain: Vec, + from_key: Vec, +} + +impl DelegatedIdentity { + /// Creates a delegated identity that signs using `to`, for the principal corresponding to the public key `from_key`. + /// + /// `chain` must be a list of delegations connecting `from_key` to `to.public_key()`, and in that order. + pub fn new(from_key: Vec, to: Box, chain: Vec) -> Self { + Self { + to, + from_key, + chain, + } + } + + fn chain_signature(&self, mut sig: Signature) -> Signature { + sig.public_key = self.public_key(); + sig.delegations + .get_or_insert(vec![]) + .extend(self.chain.iter().cloned()); + sig + } +} + +impl Identity for DelegatedIdentity { + fn sender(&self) -> Result { + Ok(Principal::self_authenticating(&self.from_key)) + } + fn public_key(&self) -> Option> { + Some(self.from_key.clone()) + } + fn sign(&self, content: &EnvelopeContent) -> Result { + self.to.sign(content).map(|sig| self.chain_signature(sig)) + } + fn sign_delegation(&self, content: &Delegation) -> Result { + self.to + .sign_delegation(content) + .map(|sig| self.chain_signature(sig)) + } + fn sign_arbitrary(&self, content: &[u8]) -> Result { + self.to + .sign_arbitrary(content) + .map(|sig| self.chain_signature(sig)) + } + fn delegation_chain(&self) -> Vec { + let mut chain = self.to.delegation_chain(); + chain.extend(self.chain.iter().cloned()); + chain + } +} diff --git a/ic-agent/src/identity/mod.rs b/ic-agent/src/identity/mod.rs index e1f539d5..84e95f1a 100644 --- a/ic-agent/src/identity/mod.rs +++ b/ic-agent/src/identity/mod.rs @@ -1,8 +1,11 @@ //! Types and traits dealing with identity across the Internet Computer. +use std::sync::Arc; + use crate::{agent::EnvelopeContent, export::Principal}; pub(crate) mod anonymous; pub(crate) mod basic; +pub(crate) mod delegated; pub(crate) mod secp256k1; #[cfg(feature = "pem")] @@ -13,7 +16,9 @@ pub use anonymous::AnonymousIdentity; #[doc(inline)] pub use basic::BasicIdentity; #[doc(inline)] -pub use ic_transport_types::Delegation; +pub use delegated::DelegatedIdentity; +#[doc(inline)] +pub use ic_transport_types::{Delegation, SignedDelegation}; #[doc(inline)] pub use secp256k1::Secp256k1Identity; @@ -27,6 +32,8 @@ pub struct Signature { pub public_key: Option>, /// The signature bytes. pub signature: Option>, + /// A list of delegations connecting `public_key` to the key that signed `signature`, and in that order. + pub delegations: Option>, } /// An `Identity` produces [`Signatures`](Signature) for requests or delegations. It knows or @@ -67,4 +74,44 @@ pub trait Identity: Send + Sync { let _ = content; // silence unused warning Err(String::from("unsupported")) } + + /// A list of signed delegations connecting [`sender`](Identity::sender) + /// to [`public_key`](Identity::public_key), and in that order. + fn delegation_chain(&self) -> Vec { + vec![] + } +} + +macro_rules! delegating_impl { + ($implementor:ty, $name:ident => $self_expr:expr) => { + impl Identity for $implementor { + fn sender(&$name) -> Result { + $self_expr.sender() + } + + fn public_key(&$name) -> Option> { + $self_expr.public_key() + } + + fn sign(&$name, content: &EnvelopeContent) -> Result { + $self_expr.sign(content) + } + + fn sign_delegation(&$name, content: &Delegation) -> Result { + $self_expr.sign_delegation(content) + } + + fn sign_arbitrary(&$name, content: &[u8]) -> Result { + $self_expr.sign_arbitrary(content) + } + + fn delegation_chain(&$name) -> Vec { + $self_expr.delegation_chain() + } + } + }; } + +delegating_impl!(Box, self => **self); +delegating_impl!(Arc, self => **self); +delegating_impl!(&dyn Identity, self => *self); diff --git a/ic-agent/src/identity/secp256k1.rs b/ic-agent/src/identity/secp256k1.rs index a7b2de91..4f05c195 100644 --- a/ic-agent/src/identity/secp256k1.rs +++ b/ic-agent/src/identity/secp256k1.rs @@ -106,6 +106,7 @@ impl Identity for Secp256k1Identity { Ok(Signature { public_key, signature, + delegations: None, }) } } diff --git a/ic-identity-hsm/src/hsm.rs b/ic-identity-hsm/src/hsm.rs index 7c02e38c..ab7dfeb4 100644 --- a/ic-identity-hsm/src/hsm.rs +++ b/ic-identity-hsm/src/hsm.rs @@ -158,6 +158,7 @@ impl Identity for HardwareIdentity { Ok(Signature { public_key: self.public_key(), signature: Some(signature), + delegations: None, }) } } diff --git a/ic-transport-types/src/lib.rs b/ic-transport-types/src/lib.rs index e0e45941..c541fb3c 100644 --- a/ic-transport-types/src/lib.rs +++ b/ic-transport-types/src/lib.rs @@ -31,7 +31,7 @@ pub struct Envelope<'a> { pub sender_sig: Option>, /// The chain of delegations connecting `sender_pubkey` to `sender_sig`, and in that order. #[serde(default, skip_serializing_if = "Option::is_none")] - pub sender_delegation: Option>, + pub sender_delegation: Option>, } /// The content of an IC ingress message, not including any signature information. @@ -236,3 +236,13 @@ impl Delegation { bytes } } + +/// A [`Delegation`] that has been signed by an [`Identity`]. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SignedDelegation { + /// The signed delegation. + pub delegation: Delegation, + /// The signature for the delegation. + #[serde(with = "serde_bytes")] + pub signature: Vec, +} diff --git a/ref-tests/src/universal_canister.rs b/ref-tests/src/universal_canister.rs index 2c62c05e..6f18034c 100644 --- a/ref-tests/src/universal_canister.rs +++ b/ref-tests/src/universal_canister.rs @@ -36,11 +36,6 @@ enum Ops { /// A succinct shortcut for creating a `PayloadBuilder`, which is used to encode /// instructions to be executed by the UC. /// -/// Note that a `PayloadBuilder` isn't really building Wasm as the name -/// of the shortcut here suggests, but we call it `wasm()` since it gives -/// a close enough indicator of what `PayloadBuilder` accomplishes without -/// getting into the details of how it accomplishes it. -/// /// Example usage: /// ``` /// use ref_tests::universal_canister::payload; diff --git a/ref-tests/src/utils.rs b/ref-tests/src/utils.rs index 5afbe59f..acc3efa4 100644 --- a/ref-tests/src/utils.rs +++ b/ref-tests/src/utils.rs @@ -15,11 +15,11 @@ pub fn get_effective_canister_id() -> Principal { Principal::from_text("rwlgt-iiaaa-aaaaa-aaaaa-cai").unwrap() } -pub async fn create_identity() -> Result, String> { +pub fn create_identity() -> Result, String> { if std::env::var(HSM_PKCS11_LIBRARY_PATH).is_ok() { - create_hsm_identity().await + create_hsm_identity().map(|x| Box::new(x) as _) } else { - create_basic_identity().await + create_basic_identity().map(|x| Box::new(x) as _) } } @@ -27,7 +27,7 @@ fn expect_env_var(name: &str) -> Result { std::env::var(name).map_err(|_| format!("Need to specify the {} environment variable", name)) } -pub async fn create_hsm_identity() -> Result, String> { +pub fn create_hsm_identity() -> Result { let path = expect_env_var(HSM_PKCS11_LIBRARY_PATH)?; let slot_index = expect_env_var(HSM_SLOT_INDEX)? .parse::() @@ -35,7 +35,7 @@ pub async fn create_hsm_identity() -> Result, String> { let key = expect_env_var(HSM_KEY_ID)?; let id = HardwareIdentity::new(path, slot_index, &key, get_hsm_pin) .map_err(|e| format!("Unable to create hw identity: {}", e))?; - Ok(Box::new(id)) + Ok(id) } fn get_hsm_pin() -> Result { @@ -49,19 +49,19 @@ fn get_hsm_pin() -> Result { // To avoid this, we use a basic identity for any second identity in tests. // // A shared container of Ctx objects might be possible instead, but my rust-fu is inadequate. -pub async fn create_basic_identity() -> Result, String> { +pub fn create_basic_identity() -> Result { let rng = ring::rand::SystemRandom::new(); let key_pair = ring::signature::Ed25519KeyPair::generate_pkcs8(&rng) .expect("Could not generate a key pair."); - Ok(Box::new(BasicIdentity::from_key_pair( + Ok(BasicIdentity::from_key_pair( Ed25519KeyPair::from_pkcs8(key_pair.as_ref()).expect("Could not read the key pair."), - ))) + )) } /// Create a secp256k1identity, which unfortunately will always be the same one /// (So can only use one per test) -pub fn create_secp256k1_identity() -> Result, String> { +pub fn create_secp256k1_identity() -> Result { // generated from the the following commands: // $ openssl ecparam -name secp256k1 -genkey -noout -out identity.pem // $ cat identity.pem @@ -74,10 +74,10 @@ yeMC60IsMNxDjLqElV7+T7dkb5Ki7Q== let identity = Secp256k1Identity::from_pem(identity_file.as_bytes()) .expect("Cannot create secp256k1 identity from PEM file."); - Ok(Box::new(identity)) + Ok(identity) } -pub async fn create_agent(identity: Box) -> Result { +pub async fn create_agent(identity: impl Identity + 'static) -> Result { let port_env = std::env::var("IC_REF_PORT").unwrap_or_else(|_| "8001".into()); let port = port_env .parse::() @@ -85,7 +85,7 @@ pub async fn create_agent(identity: Box) -> Result Agent::builder() .with_transport(ReqwestTransport::create(format!("http://127.0.0.1:{}", port)).unwrap()) - .with_boxed_identity(identity) + .with_identity(identity) .build() .map_err(|e| format!("{:?}", e)) } @@ -94,12 +94,19 @@ pub fn with_agent(f: F) where R: Future>>, F: FnOnce(Agent) -> R, +{ + let agent_identity = create_identity().expect("Could not create an identity."); + with_agent_as(agent_identity, f) +} + +pub fn with_agent_as(agent_identity: I, f: F) +where + I: Identity + 'static, + R: Future>>, + F: FnOnce(Agent) -> R, { let runtime = tokio::runtime::Runtime::new().expect("Could not create tokio runtime."); runtime.block_on(async { - let agent_identity = create_identity() - .await - .expect("Could not create an identity."); let agent = create_agent(agent_identity) .await .expect("Could not create an agent."); @@ -194,6 +201,18 @@ where }) } +pub fn with_universal_canister_as(identity: I, f: F) +where + I: Identity + 'static, + R: Future>>, + F: FnOnce(Agent, Principal) -> R, +{ + with_agent_as(identity, |agent| async move { + let canister_id = create_universal_canister(&agent).await?; + f(agent, canister_id).await + }) +} + pub fn with_wallet_canister(cycles: Option, f: F) where R: Future>>, diff --git a/ref-tests/tests/ic-ref.rs b/ref-tests/tests/ic-ref.rs index 2b31c8c8..d8da9380 100644 --- a/ref-tests/tests/ic-ref.rs +++ b/ref-tests/tests/ic-ref.rs @@ -41,7 +41,7 @@ mod management_canister { use ic_agent::{ agent::{RejectCode, RejectResponse}, export::Principal, - AgentError, + AgentError, Identity, }; use ic_utils::{ call::AsyncCall, @@ -152,7 +152,7 @@ mod management_canister { .await?; // Each agent has their own identity. - let other_agent_identity = create_basic_identity().await?; + let other_agent_identity = create_basic_identity()?; let other_agent_principal = other_agent_identity.sender()?; let other_agent = create_agent(other_agent_identity).await?; other_agent.fetch_root_key().await?; @@ -260,7 +260,7 @@ mod management_canister { with_agent(|agent| async move { let agent_principal = agent.get_principal()?; // Each agent has their own identity. - let other_agent_identity = create_basic_identity().await?; + let other_agent_identity = create_basic_identity()?; let other_agent_principal = other_agent_identity.sender()?; let other_agent = create_agent(other_agent_identity).await?; other_agent.fetch_root_key().await?; @@ -528,7 +528,7 @@ mod management_canister { .await?; // Create another agent with different identity. - let other_agent_identity = create_basic_identity().await?; + let other_agent_identity = create_basic_identity()?; let other_agent = create_agent(other_agent_identity).await?; other_agent.fetch_root_key().await?; let other_ic00 = ManagementCanister::create(&other_agent); diff --git a/ref-tests/tests/integration.rs b/ref-tests/tests/integration.rs index 9f1fc655..7b4ac15e 100644 --- a/ref-tests/tests/integration.rs +++ b/ref-tests/tests/integration.rs @@ -6,7 +6,7 @@ use candid::CandidType; use ic_agent::{ agent::{agent_error::HttpErrorPayload, RejectCode, RejectResponse}, export::Principal, - AgentError, + AgentError, Identity, }; use ic_utils::{ call::{AsyncCall, SyncCall}, @@ -192,7 +192,7 @@ fn wallet_create_and_set_controller() { .await?; // controller - let other_agent_identity = create_basic_identity().await?; + let other_agent_identity = create_basic_identity()?; let other_agent_principal = other_agent_identity.sender()?; let other_agent = create_agent(other_agent_identity).await?; other_agent.fetch_root_key().await?; @@ -418,7 +418,7 @@ fn wallet_helper_functions() { assert_eq!(name, Some(wallet_name)); // controller - let other_agent_identity = create_basic_identity().await?; + let other_agent_identity = create_basic_identity()?; let other_agent_principal = other_agent_identity.sender()?; let other_agent = create_agent(other_agent_identity).await?; other_agent.fetch_root_key().await?; @@ -575,3 +575,56 @@ mod sign_send { }) } } + +mod identity { + use candid::Principal; + use ic_agent::{ + identity::{BasicIdentity, DelegatedIdentity, Delegation, SignedDelegation}, + Identity, + }; + use ref_tests::{universal_canister::payload, with_universal_canister_as}; + use ring::{ + rand::{SecureRandom, SystemRandom}, + signature::Ed25519KeyPair, + }; + + #[ignore] + #[test] + fn delegated_identity() { + let random = SystemRandom::new(); + let mut seed = [0; 32]; + random.fill(&mut seed).unwrap(); + let sending_identity = + BasicIdentity::from_key_pair(Ed25519KeyPair::from_seed_unchecked(&seed).unwrap()); + random.fill(&mut seed).unwrap(); + let signing_identity = + BasicIdentity::from_key_pair(Ed25519KeyPair::from_seed_unchecked(&seed).unwrap()); + let delegation = Delegation { + expiration: i64::MAX as u64, + pubkey: signing_identity.public_key().unwrap(), + targets: None, + senders: None, + }; + let signature = sending_identity.sign_delegation(&delegation).unwrap(); + let delegated_identity = DelegatedIdentity::new( + signature.public_key.unwrap(), + Box::new(signing_identity), + vec![SignedDelegation { + delegation, + signature: signature.signature.unwrap(), + }], + ); + with_universal_canister_as(delegated_identity, |agent, canister| async move { + let payload = payload().caller().append_and_reply().build(); + let caller_resp = agent + .query(&canister, "query") + .with_arg(payload) + .call() + .await + .unwrap(); + let caller = Principal::from_slice(&caller_resp); + assert_eq!(caller, sending_identity.sender().unwrap()); + Ok(()) + }) + } +}