Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Check delegation in DelegatedIdentity #605

Merged
merged 2 commits into from
Oct 4, 2024
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased

* Added `AgentBuilder::with_max_polling_time` to config the maximum time to wait for a response from the replica.
* `DelegatedIdentity::new` now checks the delegation chain. The old behavior is available under `new_unchecked`.

## [0.38.2] - 2024-09-30

Expand Down
8 changes: 6 additions & 2 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ candid_parser = "0.1.1"
clap = "4.4.3"
futures-util = "0.3.21"
hex = "0.4.3"
k256 = "0.13.4"
leb128 = "0.2.5"
p256 = "0.13.2"
reqwest = { version = "0.12", default-features = false }
ring = "0.17.7"
serde = "1.0.162"
Expand Down
6 changes: 4 additions & 2 deletions ic-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,18 @@ async-watch = { version = "0.3", optional = true }
backoff = "0.4.0"
cached = { version = "0.52", features = ["ahash"], default-features = false }
candid = { workspace = true }
ecdsa = "0.16"
ed25519-consensus = { version = "2" }
elliptic-curve = "0.13"
futures-util = { workspace = true }
hex = { workspace = true }
http = "1.0.0"
http-body = "1.0.0"
ic-certification = { workspace = true }
ic-transport-types = { workspace = true }
ic-verify-bls-signature = "0.5"
k256 = { version = "0.13.1", features = ["pem"] }
p256 = { version = "0.13.2", features = ["pem"] }
k256 = { workspace = true, features = ["pem"] }
p256 = { workspace = true, features = ["pem"] }
leb128 = { workspace = true }
pkcs8 = { version = "0.10.2", features = ["std"] }
sec1 = { version = "0.7.2", features = ["pem"] }
Expand Down
99 changes: 96 additions & 3 deletions ic-agent/src/identity/delegated.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
use candid::Principal;
use ecdsa::signature::Verifier;
use k256::Secp256k1;
use p256::NistP256;
use pkcs8::{spki::SubjectPublicKeyInfoRef, AssociatedOid, ObjectIdentifier};
use sec1::{
der::{Decode, SliceReader},
EcParameters, EncodedPoint,
};

use crate::{agent::EnvelopeContent, Signature};

use super::{Delegation, Identity, SignedDelegation};
use super::{error::DelegationError, Delegation, Identity, SignedDelegation};

/// An identity that has been delegated the authority to authenticate as a different principal.
pub struct DelegatedIdentity {
Expand All @@ -14,8 +22,93 @@ pub struct DelegatedIdentity {
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 {
/// `chain` must be a list of delegations connecting `from_key` to `to.public_key()`, and in that order;
/// otherwise, this function will return an error.
pub fn new(
from_key: Vec<u8>,
to: Box<dyn Identity>,
chain: Vec<SignedDelegation>,
) -> Result<Self, DelegationError> {
let mut last_verified = &from_key;
for delegation in &chain {
let spki = SubjectPublicKeyInfoRef::decode(
&mut SliceReader::new(&last_verified[..]).map_err(|_| DelegationError::Parse)?,
)
.map_err(|_| DelegationError::Parse)?;
if spki.algorithm.oid == elliptic_curve::ALGORITHM_OID {
let Some(params) = spki.algorithm.parameters else {
return Err(DelegationError::UnknownAlgorithm);
};
let params = params
.decode_as::<EcParameters>()
.map_err(|_| DelegationError::Parse)?;
let curve = params
.named_curve()
.ok_or(DelegationError::UnknownAlgorithm)?;
if curve == Secp256k1::OID {
let pt = EncodedPoint::from_bytes(spki.subject_public_key.raw_bytes())
.map_err(|_| DelegationError::Parse)?;
let vk = k256::ecdsa::VerifyingKey::from_encoded_point(&pt)
.map_err(|_| DelegationError::Parse)?;
let sig = k256::ecdsa::Signature::try_from(&delegation.signature[..])
.map_err(|_| DelegationError::Parse)?;
vk.verify(&delegation.delegation.signable(), &sig)
.map_err(|_| DelegationError::BrokenChain {
from: last_verified.clone(),
to: Some(delegation.delegation.clone()),
})?;
} else if curve == NistP256::OID {
let pt = EncodedPoint::from_bytes(spki.subject_public_key.raw_bytes())
.map_err(|_| DelegationError::Parse)?;
let vk = p256::ecdsa::VerifyingKey::from_encoded_point(&pt)
.map_err(|_| DelegationError::Parse)?;
let sig = p256::ecdsa::Signature::try_from(&delegation.signature[..])
.map_err(|_| DelegationError::Parse)?;
vk.verify(&delegation.delegation.signable(), &sig)
.map_err(|_| DelegationError::BrokenChain {
from: last_verified.clone(),
to: Some(delegation.delegation.clone()),
})?;
} else {
return Err(DelegationError::UnknownAlgorithm);
}
} else if spki.algorithm.oid == ObjectIdentifier::new_unwrap("1.3.101.112") {
let vk = ed25519_consensus::VerificationKey::try_from(
spki.subject_public_key.raw_bytes(),
)
.map_err(|_| DelegationError::Parse)?;
let sig = ed25519_consensus::Signature::try_from(&delegation.signature[..])
.map_err(|_| DelegationError::Parse)?;
vk.verify(&sig, &delegation.delegation.signable())
.map_err(|_| DelegationError::BrokenChain {
from: last_verified.clone(),
to: Some(delegation.delegation.clone()),
})?
} else {
return Err(DelegationError::UnknownAlgorithm);
}
last_verified = &delegation.delegation.pubkey;
}
let delegated_principal = Principal::self_authenticating(last_verified);
if delegated_principal != to.sender().map_err(DelegationError::IdentityError)? {
return Err(DelegationError::BrokenChain {
from: last_verified.clone(),
to: None,
});
}

Ok(Self::new_unchecked(from_key, to, chain))
}

/// 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;
/// otherwise, the replica will reject this delegation when used as an identity.
pub fn new_unchecked(
from_key: Vec<u8>,
to: Box<dyn Identity>,
chain: Vec<SignedDelegation>,
) -> Self {
Self {
to,
from_key,
Expand Down
24 changes: 24 additions & 0 deletions ic-agent/src/identity/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
use ic_transport_types::Delegation;
use thiserror::Error;

/// An error happened while reading a PEM file.
#[cfg(feature = "pem")]
#[derive(Error, Debug)]
pub enum PemError {
/// An error occurred with disk I/O.
Expand All @@ -24,3 +26,25 @@ pub enum PemError {
#[error("A key was rejected by k256: {0}")]
ErrorStack(#[from] k256::pkcs8::Error),
}

/// An error occurred constructing a [`DelegatedIdentity`](super::delegated::DelegatedIdentity).
#[derive(Error, Debug)]
pub enum DelegationError {
/// Parsing error in delegation bytes.
#[error("A delegation could not be parsed")]
Parse,
/// A key in the chain did not match the signature of the next chain link.
#[error("A link was missing in the delegation chain")]
BrokenChain {
/// The key that should have matched the next delegation
from: Vec<u8>,
/// The delegation that didn't match, or `None` if the `Identity` didn't match
to: Option<Delegation>,
},
/// A key with an unknown algorithm was used. The IC supports Ed25519, secp256k1, and prime256v1, and in ECDSA the curve must be specified.
#[error("The delegation chain contained a key with an unknown algorithm")]
UnknownAlgorithm,
/// One of `Identity`'s functions returned an error.
#[error("A delegated-to identity encountered an error: {0}")]
IdentityError(String),
}
7 changes: 4 additions & 3 deletions ic-agent/src/identity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,27 @@ use crate::{agent::EnvelopeContent, export::Principal};
pub(crate) mod anonymous;
pub(crate) mod basic;
pub(crate) mod delegated;
pub(crate) mod error;
pub(crate) mod prime256v1;
pub(crate) mod secp256k1;

#[cfg(feature = "pem")]
pub(crate) mod error;

#[doc(inline)]
pub use anonymous::AnonymousIdentity;
#[doc(inline)]
pub use basic::BasicIdentity;
#[doc(inline)]
pub use delegated::DelegatedIdentity;
#[doc(inline)]
pub use error::DelegationError;
#[doc(inline)]
pub use ic_transport_types::{Delegation, SignedDelegation};
#[doc(inline)]
pub use prime256v1::Prime256v1Identity;
#[doc(inline)]
pub use secp256k1::Secp256k1Identity;

#[cfg(feature = "pem")]
#[doc(inline)]
pub use error::PemError;

/// A cryptographic signature, signed by an [Identity].
Expand Down
2 changes: 2 additions & 0 deletions ref-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ candid = { workspace = true }
ic-agent = { path = "../ic-agent" }
ic-identity-hsm = { path = "../ic-identity-hsm" }
ic-utils = { path = "../ic-utils", features = ["raw"] }
k256 = { workspace = true }
p256 = { workspace = true }
ring = { workspace = true }
serde = { workspace = true, features = ["derive"] }
sha2 = { workspace = true }
Expand Down
49 changes: 47 additions & 2 deletions ref-tests/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,10 @@ mod sign_send {
mod identity {
use candid::Principal;
use ic_agent::{
identity::{BasicIdentity, DelegatedIdentity, Delegation, SignedDelegation},
identity::{
BasicIdentity, DelegatedIdentity, Delegation, Prime256v1Identity, Secp256k1Identity,
SignedDelegation,
},
Identity,
};
use ref_tests::{universal_canister::payload, with_universal_canister_as};
Expand All @@ -685,7 +688,7 @@ mod identity {

#[ignore]
#[test]
fn delegated_identity() {
fn delegated_eddsa_identity() {
let random = SystemRandom::new();
let mut seed = [0; 32];
random.fill(&mut seed).unwrap();
Expand All @@ -707,7 +710,49 @@ mod identity {
delegation,
signature: signature.signature.unwrap(),
}],
)
.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(())
})
}

#[ignore]
#[test]
fn delegated_ecdsa_identity() {
let random = SystemRandom::new();
let mut seed = [0; 32];
random.fill(&mut seed).unwrap();
let sending_identity =
Secp256k1Identity::from_private_key(k256::SecretKey::from_bytes(&seed.into()).unwrap());
random.fill(&mut seed).unwrap();
let signing_identity = Prime256v1Identity::from_private_key(
p256::SecretKey::from_bytes(&seed.into()).unwrap(),
);
let delegation = Delegation {
expiration: i64::MAX as u64,
pubkey: signing_identity.public_key().unwrap(),
targets: 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(),
}],
)
.unwrap();
with_universal_canister_as(delegated_identity, |agent, canister| async move {
let payload = payload().caller().append_and_reply().build();
let caller_resp = agent
Expand Down
Loading