From 87ccf27c335a10cf813daff988cd08cbaaafe68d Mon Sep 17 00:00:00 2001 From: Adam Spofford Date: Wed, 24 Jan 2024 13:23:15 -0800 Subject: [PATCH] Add Prime256v1Identity --- CHANGELOG.md | 2 + Cargo.lock | 22 +++ ic-agent/Cargo.toml | 1 + ic-agent/src/identity/error.rs | 4 +- ic-agent/src/identity/mod.rs | 3 + ic-agent/src/identity/prime256v1.rs | 228 ++++++++++++++++++++++++++++ ic-agent/src/identity/secp256k1.rs | 5 +- ref-tests/src/utils.rs | 18 ++- ref-tests/tests/ic-ref.rs | 50 +++++- 9 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 ic-agent/src/identity/prime256v1.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8df17bfa..3c7d33bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +* Added a prime256v1-based `Identity` impl to complement the ed25519 and secp256k1 `Identity` impls. + ## [0.32.0] - 2024-01-18 * Added the chunked wasm API to ic-utils. Existing code that uses `install_code` should probably update to `install`, which works the same but silently handles large wasm modules. diff --git a/Cargo.lock b/Cargo.lock index f30c3b08..51103261 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1071,6 +1071,7 @@ dependencies = [ "k256", "leb128", "mockito", + "p256", "pem", "pkcs8", "rand", @@ -1559,6 +1560,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.7", +] + [[package]] name = "pairing" version = "0.22.0" @@ -1702,6 +1715,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "primeorder" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7dbe9ed3b56368bd99483eb32fe9c17fdd3730aebadc906918ce78d54c7eeb4" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.66" diff --git a/ic-agent/Cargo.toml b/ic-agent/Cargo.toml index 6bb26406..9af39a73 100644 --- a/ic-agent/Cargo.toml +++ b/ic-agent/Cargo.toml @@ -27,6 +27,7 @@ ic-certification = { workspace = true } ic-transport-types = { workspace = true } ic-verify-bls-signature = "0.1" k256 = { version = "0.13.1", features = ["pem"] } +p256 = { version = "0.13.2", 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/error.rs b/ic-agent/src/identity/error.rs index 5fa46dbe..592d1e5c 100644 --- a/ic-agent/src/identity/error.rs +++ b/ic-agent/src/identity/error.rs @@ -8,8 +8,8 @@ pub enum PemError { Io(#[from] std::io::Error), /// An unsupported curve was detected - #[error("Only secp256k1 curve is supported: {0:?}")] - UnsupportedKeyCurve(Vec), + #[error("Only {0} curve is supported: {1:?}")] + UnsupportedKeyCurve(String, Vec), /// An error occurred while reading the file in PEM format. #[cfg(feature = "pem")] diff --git a/ic-agent/src/identity/mod.rs b/ic-agent/src/identity/mod.rs index 84e95f1a..ed2dd4e3 100644 --- a/ic-agent/src/identity/mod.rs +++ b/ic-agent/src/identity/mod.rs @@ -6,6 +6,7 @@ use crate::{agent::EnvelopeContent, export::Principal}; pub(crate) mod anonymous; pub(crate) mod basic; pub(crate) mod delegated; +pub(crate) mod prime256v1; pub(crate) mod secp256k1; #[cfg(feature = "pem")] @@ -20,6 +21,8 @@ pub use delegated::DelegatedIdentity; #[doc(inline)] pub use ic_transport_types::{Delegation, SignedDelegation}; #[doc(inline)] +pub use prime256v1::Prime256v1Identity; +#[doc(inline)] pub use secp256k1::Secp256k1Identity; #[cfg(feature = "pem")] diff --git a/ic-agent/src/identity/prime256v1.rs b/ic-agent/src/identity/prime256v1.rs new file mode 100644 index 00000000..4cec9a0b --- /dev/null +++ b/ic-agent/src/identity/prime256v1.rs @@ -0,0 +1,228 @@ +use crate::{agent::EnvelopeContent, export::Principal, Identity, Signature}; + +#[cfg(feature = "pem")] +use crate::identity::error::PemError; + +use p256::{ + ecdsa::{self, signature::Signer, SigningKey, VerifyingKey}, + pkcs8::{Document, EncodePublicKey}, + SecretKey, +}; +#[cfg(feature = "pem")] +use std::{fs::File, io, path::Path}; + +use super::Delegation; + +/// A cryptographic identity based on the Prime256v1 elliptic curve. +/// +/// The caller will be represented via [`Principal::self_authenticating`], which contains the SHA-224 hash of the public key. +#[derive(Clone, Debug)] +pub struct Prime256v1Identity { + private_key: SigningKey, + _public_key: VerifyingKey, + der_encoded_public_key: Document, +} + +impl Prime256v1Identity { + /// Creates an identity from a PEM file. Shorthand for calling `from_pem` with `std::fs::read`. + #[cfg(feature = "pem")] + pub fn from_pem_file>(file_path: P) -> Result { + Self::from_pem(File::open(file_path)?) + } + + /// Creates an identity from a PEM certificate. + #[cfg(feature = "pem")] + pub fn from_pem(pem_reader: R) -> Result { + use sec1::{pem::PemLabel, EcPrivateKey}; + + const EC_PARAMETERS: &str = "EC PARAMETERS"; + const PRIME256V1: &[u8] = b"\x06\x08\x2a\x86\x48\xce\x3d\x03\x01\x07"; + + let contents = pem_reader.bytes().collect::, io::Error>>()?; + + for pem in pem::parse_many(contents)? { + if pem.tag() == EC_PARAMETERS && pem.contents() != PRIME256V1 { + return Err(PemError::UnsupportedKeyCurve( + "prime256v1".to_string(), + pem.contents().to_vec(), + )); + } + + if pem.tag() != EcPrivateKey::PEM_LABEL { + continue; + } + let private_key = + SecretKey::from_sec1_der(pem.contents()).map_err(|_| pkcs8::Error::KeyMalformed)?; + return Ok(Self::from_private_key(private_key)); + } + Err(pem::PemError::MissingData.into()) + } + + /// Creates an identity from a private key. + pub fn from_private_key(private_key: SecretKey) -> Self { + let public_key = private_key.public_key(); + let der_encoded_public_key = public_key + .to_public_key_der() + .expect("Cannot DER encode prime256v1 public key."); + Self { + private_key: private_key.into(), + _public_key: public_key.into(), + der_encoded_public_key, + } + } +} + +impl Identity for Prime256v1Identity { + fn sender(&self) -> Result { + Ok(Principal::self_authenticating( + self.der_encoded_public_key.as_ref(), + )) + } + + fn public_key(&self) -> Option> { + Some(self.der_encoded_public_key.as_ref().to_vec()) + } + + fn sign(&self, content: &EnvelopeContent) -> Result { + self.sign_arbitrary(&content.to_request_id().signable()) + } + + fn sign_delegation(&self, content: &Delegation) -> Result { + self.sign_arbitrary(&content.signable()) + } + + fn sign_arbitrary(&self, content: &[u8]) -> Result { + let ecdsa_sig: ecdsa::Signature = self + .private_key + .try_sign(content) + .map_err(|err| format!("Cannot create prime256v1 signature: {}", err))?; + let r = ecdsa_sig.r().as_ref().to_bytes(); + let s = ecdsa_sig.s().as_ref().to_bytes(); + let mut bytes = [0u8; 64]; + if r.len() > 32 || s.len() > 32 { + return Err("Cannot create prime256v1 signature: malformed signature.".to_string()); + } + bytes[(32 - r.len())..32].clone_from_slice(&r); + bytes[32 + (32 - s.len())..].clone_from_slice(&s); + let signature = Some(bytes.to_vec()); + let public_key = self.public_key(); + Ok(Signature { + public_key, + signature, + delegations: None, + }) + } +} + +#[cfg(feature = "pem")] +#[cfg(test)] +mod test { + use super::*; + use candid::Encode; + use p256::{ + ecdsa::{signature::Verifier, Signature}, + elliptic_curve::PrimeField, + FieldBytes, Scalar, + }; + + // WRONG_CURVE_IDENTITY_FILE is generated from the following command: + // > openssl ecparam -name secp160r2 -genkey + // it uses the secp160r2 curve instead of prime256v1 and should + // therefore be rejected by Prime256v1Identity when loading an identity + const WRONG_CURVE_IDENTITY_FILE: &str = "\ +-----BEGIN EC PARAMETERS----- +BgUrgQQAHg== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR +oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ== +-----END EC PRIVATE KEY----- +"; + + // WRONG_CURVE_IDENTITY_FILE_NO_PARAMS is generated from the following command: + // > openssl ecparam -name secp160r2 -genkey -noout + // it uses the secp160r2 curve instead of prime256v1 and should + // therefore be rejected by Prime256v1Identity when loading an identity + const WRONG_CURVE_IDENTITY_FILE_NO_PARAMS: &str = "\ +-----BEGIN EC PRIVATE KEY----- +MFACAQEEFI9cF6zXxMKhtjn1gBD7AHPbzehfoAcGBSuBBAAeoSwDKgAEh5NXszgR +oGSXVWaGxcQhQWlFG4pbnOG+93xXzfRD7eKWOdmun2bKxQ== +-----END EC PRIVATE KEY----- +"; + + // IDENTITY_FILE was generated from the the following commands: + // > openssl ecparam -name prime256v1 -genkey -noout -out identity.pem + // > cat identity.pem + const IDENTITY_FILE: &str = "\ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIL1ybmbwx+uKYsscOZcv71MmKhrNqfPP0ke1unET5AY4oAoGCCqGSM49 +AwEHoUQDQgAEUbbZV4NerZTPWfbQ749/GNLu8TaH8BUS/I7/+ipsu+MPywfnBFIZ +Sks4xGbA/ZbazsrMl4v446U5UIVxCGGaKw== +-----END EC PRIVATE KEY----- +"; + + // DER_ENCODED_PUBLIC_KEY was generated from the the following commands: + // > openssl ec -in identity.pem -pubout -outform DER -out public.der + // > hexdump -ve '1/1 "%.2x"' public.der + const DER_ENCODED_PUBLIC_KEY: &str = "3059301306072a8648ce3d020106082a8648ce3d0301070342000451b6d957835ead94cf59f6d0ef8f7f18d2eef13687f01512fc8efffa2a6cbbe30fcb07e70452194a4b38c466c0fd96dacecacc978bf8e3a53950857108619a2b"; + + #[test] + #[should_panic(expected = "UnsupportedKeyCurve")] + fn test_prime256v1_reject_wrong_curve() { + Prime256v1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE.as_bytes()).unwrap(); + } + + #[test] + #[should_panic(expected = "KeyMalformed")] + fn test_prime256v1_reject_wrong_curve_no_id() { + Prime256v1Identity::from_pem(WRONG_CURVE_IDENTITY_FILE_NO_PARAMS.as_bytes()).unwrap(); + } + + #[test] + fn test_prime256v1_public_key() { + // Create a prime256v1 identity from a PEM file. + let identity = Prime256v1Identity::from_pem(IDENTITY_FILE.as_bytes()) + .expect("Cannot create prime256v1 identity from PEM file."); + + // Assert the DER-encoded prime256v1 public key matches what we would expect. + assert!(DER_ENCODED_PUBLIC_KEY == hex::encode(identity.der_encoded_public_key)); + } + + #[test] + fn test_prime256v1_signature() { + // Create a prime256v1 identity from a PEM file. + let identity = Prime256v1Identity::from_pem(IDENTITY_FILE.as_bytes()) + .expect("Cannot create prime256v1 identity from PEM file."); + + // Create a prime256v1 signature for a hello-world canister. + let message = EnvelopeContent::Call { + nonce: None, + ingress_expiry: 0, + sender: identity.sender().unwrap(), + canister_id: "bkyz2-fmaaa-aaaaa-qaaaq-cai".parse().unwrap(), + method_name: "greet".to_string(), + arg: Encode!(&"world").unwrap(), + }; + let signature = identity + .sign(&message) + .expect("Cannot create prime256v1 signature.") + .signature + .expect("Cannot find prime256v1 signature bytes."); + + // Import the prime256v1 signature. + let r: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice( + &signature[0..32], + ))) + .expect("Cannot extract r component from prime256v1 signature bytes."); + let s: Scalar = Option::from(Scalar::from_repr(*FieldBytes::from_slice(&signature[32..]))) + .expect("Cannot extract s component from prime256v1 signature bytes."); + let ecdsa_sig = Signature::from_scalars(r, s) + .expect("Cannot create prime256v1 signature from r and s components."); + + // Assert the prime256v1 signature is valid. + identity + ._public_key + .verify(&message.to_request_id().signable(), &ecdsa_sig) + .expect("Cannot verify prime256v1 signature."); + } +} diff --git a/ic-agent/src/identity/secp256k1.rs b/ic-agent/src/identity/secp256k1.rs index 4f05c195..73670987 100644 --- a/ic-agent/src/identity/secp256k1.rs +++ b/ic-agent/src/identity/secp256k1.rs @@ -42,7 +42,10 @@ impl Secp256k1Identity { for pem in pem::parse_many(contents)? { if pem.tag() == EC_PARAMETERS && pem.contents() != SECP256K1 { - return Err(PemError::UnsupportedKeyCurve(pem.contents().to_vec())); + return Err(PemError::UnsupportedKeyCurve( + "secp256k1".to_string(), + pem.contents().to_vec(), + )); } if pem.tag() != EcPrivateKey::PEM_LABEL { diff --git a/ref-tests/src/utils.rs b/ref-tests/src/utils.rs index acc3efa4..d2746a35 100644 --- a/ref-tests/src/utils.rs +++ b/ref-tests/src/utils.rs @@ -1,5 +1,5 @@ use ic_agent::agent::http_transport::ReqwestTransport; -use ic_agent::identity::Secp256k1Identity; +use ic_agent::identity::{Prime256v1Identity, Secp256k1Identity}; use ic_agent::{export::Principal, identity::BasicIdentity, Agent, Identity}; use ic_identity_hsm::HardwareIdentity; use ic_utils::interfaces::{management_canister::builders::MemoryAllocation, ManagementCanister}; @@ -77,6 +77,22 @@ yeMC60IsMNxDjLqElV7+T7dkb5Ki7Q== Ok(identity) } +pub fn create_prime256v1_identity() -> Result { + // generated from the following command: + // $ openssl ecparam -name prime256v1 -genkey -noout -out identity.pem + // $ cat identity.pem + let identity_file = "\ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIL1ybmbwx+uKYsscOZcv71MmKhrNqfPP0ke1unET5AY4oAoGCCqGSM49 +AwEHoUQDQgAEUbbZV4NerZTPWfbQ749/GNLu8TaH8BUS/I7/+ipsu+MPywfnBFIZ +Sks4xGbA/ZbazsrMl4v446U5UIVxCGGaKw== +-----END EC PRIVATE KEY-----"; + + let identity = Prime256v1Identity::from_pem(identity_file.as_bytes()) + .expect("Cannot create prime256v1 identity from PEM file."); + Ok(identity) +} + 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 diff --git a/ref-tests/tests/ic-ref.rs b/ref-tests/tests/ic-ref.rs index 5b03214e..1c794518 100644 --- a/ref-tests/tests/ic-ref.rs +++ b/ref-tests/tests/ic-ref.rs @@ -42,8 +42,8 @@ mod management_canister { Argument, }; use ref_tests::{ - create_agent, create_basic_identity, create_secp256k1_identity, with_agent, - with_wallet_canister, + create_agent, create_basic_identity, create_prime256v1_identity, create_secp256k1_identity, + with_agent, with_wallet_canister, }; use ref_tests::{get_effective_canister_id, with_universal_canister}; use sha2::{Digest, Sha256}; @@ -274,6 +274,12 @@ mod management_canister { secp256k1_agent.fetch_root_key().await?; let secp256k1_ic00 = ManagementCanister::create(&secp256k1_agent); + let prime256v1_identity = create_prime256v1_identity()?; + let prime256v1_principal = prime256v1_identity.sender()?; + let prime256v1_agent = create_agent(prime256v1_identity).await?; + prime256v1_agent.fetch_root_key().await?; + let prime256v1_ic00 = ManagementCanister::create(&prime256v1_agent); + let ic00 = ManagementCanister::create(&agent); let (canister_id,) = ic00 @@ -348,6 +354,46 @@ mod management_canister { assert_eq!(result.0.settings.controllers.len(), 1); assert_eq!(result.0.settings.controllers[0], secp256k1_principal); + // Only that controller can change the controller again + let result = ic00 + .update_settings(&canister_id) + .with_controller(prime256v1_principal) + .call_and_wait() + .await; + assert_err_or_reject( + result, + vec![RejectCode::DestinationInvalid, RejectCode::CanisterError], + ); + let result = other_ic00 + .update_settings(&canister_id) + .with_controller(prime256v1_principal) + .call_and_wait() + .await; + assert_err_or_reject( + result, + vec![RejectCode::DestinationInvalid, RejectCode::CanisterError], + ); + + secp256k1_ic00 + .update_settings(&canister_id) + .with_controller(prime256v1_principal) + .call_and_wait() + .await?; + let result = secp256k1_ic00 + .canister_status(&canister_id) + .call_and_wait() + .await; + assert_err_or_reject( + result, + vec![RejectCode::DestinationInvalid, RejectCode::CanisterError], + ); + let result = prime256v1_ic00 + .canister_status(&canister_id) + .call_and_wait() + .await?; + assert_eq!(result.0.settings.controllers.len(), 1); + assert_eq!(result.0.settings.controllers[0], prime256v1_principal); + Ok(()) }) }