diff --git a/CHANGELOG.md b/CHANGELOG.md index 90bf488f..ef23df17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 1fe7d4fd..110e0cd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1054,7 +1054,9 @@ dependencies = [ "backoff", "cached", "candid", + "ecdsa", "ed25519-consensus", + "elliptic-curve", "futures-util", "getrandom", "hex", @@ -1307,9 +1309,9 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" dependencies = [ "cfg-if", "ecdsa", @@ -1931,6 +1933,8 @@ dependencies = [ "ic-certification", "ic-identity-hsm", "ic-utils", + "k256", + "p256", "ring", "serde", "serde_cbor", diff --git a/Cargo.toml b/Cargo.toml index 07276902..02767256 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/ic-agent/Cargo.toml b/ic-agent/Cargo.toml index 7fa64e3f..c1de4d55 100644 --- a/ic-agent/Cargo.toml +++ b/ic-agent/Cargo.toml @@ -23,7 +23,9 @@ 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" @@ -31,8 +33,8 @@ 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"] } diff --git a/ic-agent/src/identity/delegated.rs b/ic-agent/src/identity/delegated.rs index 623b03c8..0ea7638e 100644 --- a/ic-agent/src/identity/delegated.rs +++ b/ic-agent/src/identity/delegated.rs @@ -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 { @@ -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, to: Box, chain: Vec) -> 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, + to: Box, + chain: Vec, + ) -> Result { + 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::() + .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, + to: Box, + chain: Vec, + ) -> Self { Self { to, from_key, diff --git a/ic-agent/src/identity/error.rs b/ic-agent/src/identity/error.rs index 592d1e5c..cd14c895 100644 --- a/ic-agent/src/identity/error.rs +++ b/ic-agent/src/identity/error.rs @@ -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. @@ -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, + /// The delegation that didn't match, or `None` if the `Identity` didn't match + to: Option, + }, + /// 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), +} diff --git a/ic-agent/src/identity/mod.rs b/ic-agent/src/identity/mod.rs index ed2dd4e3..45c9a301 100644 --- a/ic-agent/src/identity/mod.rs +++ b/ic-agent/src/identity/mod.rs @@ -6,12 +6,10 @@ 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)] @@ -19,6 +17,8 @@ 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; @@ -26,6 +26,7 @@ pub use prime256v1::Prime256v1Identity; pub use secp256k1::Secp256k1Identity; #[cfg(feature = "pem")] +#[doc(inline)] pub use error::PemError; /// A cryptographic signature, signed by an [Identity]. diff --git a/ref-tests/Cargo.toml b/ref-tests/Cargo.toml index c4505767..e2c6397e 100644 --- a/ref-tests/Cargo.toml +++ b/ref-tests/Cargo.toml @@ -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 } diff --git a/ref-tests/tests/integration.rs b/ref-tests/tests/integration.rs index 8ad2b4ec..19362cf8 100644 --- a/ref-tests/tests/integration.rs +++ b/ref-tests/tests/integration.rs @@ -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}; @@ -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(); @@ -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