Skip to content

Commit

Permalink
Check delegation in DelegatedIdentity
Browse files Browse the repository at this point in the history
  • Loading branch information
adamspofford-dfinity committed Oct 3, 2024
1 parent a56ccd4 commit 1a945c0
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 9 deletions.
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
21 changes: 21 additions & 0 deletions ic-agent/src/identity/error.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use ic_transport_types::Delegation;
use thiserror::Error;

/// An error happened while reading a PEM file.
Expand All @@ -24,3 +25,23 @@ 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. If `to` is `None` then it refers to the `Identity`.
#[error("A link was missing in the delegation chain")]
BrokenChain {
from: Vec<u8>,
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),
}
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

0 comments on commit 1a945c0

Please sign in to comment.