Skip to content

Commit

Permalink
feat: DelegatedIdentity (#471)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamspofford-dfinity committed Sep 20, 2023
1 parent 7a67191 commit 3658229
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 30 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion ic-agent/src/agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
2 changes: 2 additions & 0 deletions ic-agent/src/identity/anonymous.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ impl Identity for AnonymousIdentity {
Ok(Signature {
signature: None,
public_key: None,
delegations: None,
})
}

fn sign_arbitrary(&self, _: &[u8]) -> Result<Signature, String> {
Ok(Signature {
public_key: None,
signature: None,
delegations: None,
})
}
}
1 change: 1 addition & 0 deletions ic-agent/src/identity/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl Identity for BasicIdentity {
Ok(Signature {
signature: Some(signature.as_ref().to_vec()),
public_key: self.public_key(),
delegations: None,
})
}
}
Expand Down
60 changes: 60 additions & 0 deletions ic-agent/src/identity/delegated.rs
Original file line number Diff line number Diff line change
@@ -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<dyn Identity>,
chain: Vec<SignedDelegation>,
from_key: Vec<u8>,
}

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<u8>, to: Box<dyn Identity>, chain: Vec<SignedDelegation>) -> 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<Principal, String> {
Ok(Principal::self_authenticating(&self.from_key))
}
fn public_key(&self) -> Option<Vec<u8>> {
Some(self.from_key.clone())
}
fn sign(&self, content: &EnvelopeContent) -> Result<Signature, String> {
self.to.sign(content).map(|sig| self.chain_signature(sig))
}
fn sign_delegation(&self, content: &Delegation) -> Result<Signature, String> {
self.to
.sign_delegation(content)
.map(|sig| self.chain_signature(sig))
}
fn sign_arbitrary(&self, content: &[u8]) -> Result<Signature, String> {
self.to
.sign_arbitrary(content)
.map(|sig| self.chain_signature(sig))
}
fn delegation_chain(&self) -> Vec<SignedDelegation> {
let mut chain = self.to.delegation_chain();
chain.extend(self.chain.iter().cloned());
chain
}
}
49 changes: 48 additions & 1 deletion ic-agent/src/identity/mod.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand All @@ -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;

Expand All @@ -27,6 +32,8 @@ pub struct Signature {
pub public_key: Option<Vec<u8>>,
/// The signature bytes.
pub signature: Option<Vec<u8>>,
/// A list of delegations connecting `public_key` to the key that signed `signature`, and in that order.
pub delegations: Option<Vec<SignedDelegation>>,
}

/// An `Identity` produces [`Signatures`](Signature) for requests or delegations. It knows or
Expand Down Expand Up @@ -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<SignedDelegation> {
vec![]
}
}

macro_rules! delegating_impl {
($implementor:ty, $name:ident => $self_expr:expr) => {
impl Identity for $implementor {
fn sender(&$name) -> Result<Principal, String> {
$self_expr.sender()
}

fn public_key(&$name) -> Option<Vec<u8>> {
$self_expr.public_key()
}

fn sign(&$name, content: &EnvelopeContent) -> Result<Signature, String> {
$self_expr.sign(content)
}

fn sign_delegation(&$name, content: &Delegation) -> Result<Signature, String> {
$self_expr.sign_delegation(content)
}

fn sign_arbitrary(&$name, content: &[u8]) -> Result<Signature, String> {
$self_expr.sign_arbitrary(content)
}

fn delegation_chain(&$name) -> Vec<SignedDelegation> {
$self_expr.delegation_chain()
}
}
};
}

delegating_impl!(Box<dyn Identity>, self => **self);
delegating_impl!(Arc<dyn Identity>, self => **self);
delegating_impl!(&dyn Identity, self => *self);
1 change: 1 addition & 0 deletions ic-agent/src/identity/secp256k1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ impl Identity for Secp256k1Identity {
Ok(Signature {
public_key,
signature,
delegations: None,
})
}
}
Expand Down
1 change: 1 addition & 0 deletions ic-identity-hsm/src/hsm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ impl Identity for HardwareIdentity {
Ok(Signature {
public_key: self.public_key(),
signature: Some(signature),
delegations: None,
})
}
}
Expand Down
12 changes: 11 additions & 1 deletion ic-transport-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub struct Envelope<'a> {
pub sender_sig: Option<Vec<u8>>,
/// 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<Vec<Delegation>>,
pub sender_delegation: Option<Vec<SignedDelegation>>,
}

/// The content of an IC ingress message, not including any signature information.
Expand Down Expand Up @@ -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<u8>,
}
5 changes: 0 additions & 5 deletions ref-tests/src/universal_canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
49 changes: 34 additions & 15 deletions ref-tests/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,27 @@ pub fn get_effective_canister_id() -> Principal {
Principal::from_text("rwlgt-iiaaa-aaaaa-aaaaa-cai").unwrap()
}

pub async fn create_identity() -> Result<Box<dyn Identity>, String> {
pub fn create_identity() -> Result<Box<dyn Identity>, 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 _)
}
}

fn expect_env_var(name: &str) -> Result<String, String> {
std::env::var(name).map_err(|_| format!("Need to specify the {} environment variable", name))
}

pub async fn create_hsm_identity() -> Result<Box<dyn Identity>, String> {
pub fn create_hsm_identity() -> Result<HardwareIdentity, String> {
let path = expect_env_var(HSM_PKCS11_LIBRARY_PATH)?;
let slot_index = expect_env_var(HSM_SLOT_INDEX)?
.parse::<usize>()
.map_err(|e| format!("Unable to parse {} value: {}", HSM_SLOT_INDEX, e))?;
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<String, String> {
Expand All @@ -49,19 +49,19 @@ fn get_hsm_pin() -> Result<String, String> {
// 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<Box<dyn Identity>, String> {
pub fn create_basic_identity() -> Result<BasicIdentity, String> {
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<Box<dyn Identity + Send + Sync>, String> {
pub fn create_secp256k1_identity() -> Result<Secp256k1Identity, String> {
// generated from the the following commands:
// $ openssl ecparam -name secp256k1 -genkey -noout -out identity.pem
// $ cat identity.pem
Expand All @@ -74,18 +74,18 @@ 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<dyn Identity>) -> Result<Agent, String> {
pub async fn create_agent(identity: impl Identity + 'static) -> Result<Agent, String> {
let port_env = std::env::var("IC_REF_PORT").unwrap_or_else(|_| "8001".into());
let port = port_env
.parse::<u32>()
.expect("Could not parse the IC_REF_PORT environment variable as an integer.");

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))
}
Expand All @@ -94,12 +94,19 @@ pub fn with_agent<F, R>(f: F)
where
R: Future<Output = Result<(), Box<dyn Error>>>,
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<I, F, R>(agent_identity: I, f: F)
where
I: Identity + 'static,
R: Future<Output = Result<(), Box<dyn Error>>>,
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.");
Expand Down Expand Up @@ -194,6 +201,18 @@ where
})
}

pub fn with_universal_canister_as<I, F, R>(identity: I, f: F)
where
I: Identity + 'static,
R: Future<Output = Result<(), Box<dyn Error>>>,
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<F, R>(cycles: Option<u128>, f: F)
where
R: Future<Output = Result<(), Box<dyn Error>>>,
Expand Down
8 changes: 4 additions & 4 deletions ref-tests/tests/ic-ref.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ mod management_canister {
use ic_agent::{
agent::{RejectCode, RejectResponse},
export::Principal,
AgentError,
AgentError, Identity,
};
use ic_utils::{
call::AsyncCall,
Expand Down Expand Up @@ -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?;
Expand Down Expand Up @@ -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?;
Expand Down Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 3658229

Please sign in to comment.