From d0f734d4249468662ab79521b32aa3754d82b22a Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 19 Oct 2026 15:59:50 +0000 Subject: [PATCH 1/7] feat(crypto): support the mainnet secp256r1 ECDSA keys in ic-pub-key The mainnet ECDSA keys `key_1` and `test_key_1` now also exist over secp256r1, but `ic-pub-key` derives keys offline only for secp256k1. * `ic-secp256r1` gains `PublicKey::mainnet_key` with the master public keys of `ecdsa:Secp256r1:key_1` and `ecdsa:Secp256r1:test_key_1`, read from the CUPs of subnets uzr34 and fuqsr. * `ic-pub-key` derives keys for both behind a new default `secp256r1` feature, and moves from the workspace's `ic-management-canister-types` 0.8.0 to the in-repo 0.11.0, the first version with `EcdsaCurve::Secp256r1`. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 3 +- packages/ic-pub-key/BUILD.bazel | 8 +++- packages/ic-pub-key/CHANGELOG.md | 10 ++++ packages/ic-pub-key/Cargo.toml | 6 ++- packages/ic-pub-key/src/lib.rs | 71 +++++++++++++++++++++++++++- packages/ic-pub-key/tests/tests.rs | 60 ++++++++++++++++++++++- packages/ic-secp256r1/BUILD.bazel | 1 + packages/ic-secp256r1/CHANGELOG.md | 6 +++ packages/ic-secp256r1/Cargo.toml | 1 + packages/ic-secp256r1/src/lib.rs | 24 ++++++++++ packages/ic-secp256r1/tests/tests.rs | 14 ++++++ 11 files changed, 197 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a1874d8858e..b65afc8ebfac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12597,8 +12597,9 @@ dependencies = [ "hex", "ic-cdk-management-canister", "ic-ed25519 0.6.0", - "ic-management-canister-types 0.8.0", + "ic-management-canister-types 0.11.0", "ic-secp256k1", + "ic-secp256r1", "ic-vetkeys", ] diff --git a/packages/ic-pub-key/BUILD.bazel b/packages/ic-pub-key/BUILD.bazel index c3c4581c08bb..b86f66609850 100644 --- a/packages/ic-pub-key/BUILD.bazel +++ b/packages/ic-pub-key/BUILD.bazel @@ -7,6 +7,7 @@ rust_library( srcs = glob(["src/**/*.rs"]), crate_features = [ "secp256k1", + "secp256r1", "ed25519", "vetkeys", ], @@ -14,9 +15,10 @@ rust_library( version = "0.3.0", deps = [ "//packages/ic-ed25519", + "//packages/ic-management-canister-types", "//packages/ic-secp256k1", + "//packages/ic-secp256r1", "@crate_index//:ic-cdk-management-canister", - "@crate_index//:ic-management-canister-types", "@crate_index//:ic-vetkeys", ], ) @@ -29,6 +31,7 @@ rust_doc( crate = ":ic-pub-key", rustdoc_flags = [ "--cfg=feature=\"secp256k1\"", + "--cfg=feature=\"secp256r1\"", "--cfg=feature=\"ed25519\"", "--cfg=feature=\"vetkeys\"", ], @@ -39,6 +42,7 @@ rust_doc_test( crate = ":ic-pub-key", crate_features = [ "secp256k1", + "secp256r1", "ed25519", "vetkeys", ], @@ -50,6 +54,7 @@ rust_test( crate = ":ic-pub-key", crate_features = [ "secp256k1", + "secp256r1", "ed25519", "vetkeys", ], @@ -60,6 +65,7 @@ rust_test_suite( srcs = glob(["tests/**/*.rs"]), crate_features = [ "secp256k1", + "secp256r1", "ed25519", "vetkeys", ], diff --git a/packages/ic-pub-key/CHANGELOG.md b/packages/ic-pub-key/CHANGELOG.md index cb8ab1d185ee..fcb95ddf8250 100644 --- a/packages/ic-pub-key/CHANGELOG.md +++ b/packages/ic-pub-key/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Add offline derivation for the mainnet ECDSA keys `key_1` and `test_key_1` over the secp256r1 curve, behind the new (default) `secp256r1` feature. + +### Changed + +- Upgrade the `ic-management-canister-types` dependency from `0.8.0` to `0.11.0`, whose `EcdsaCurve` has the `Secp256r1` variant. The re-exported types now come from that version. + ## [0.3.0] - 2026-02-04 ### Changed diff --git a/packages/ic-pub-key/Cargo.toml b/packages/ic-pub-key/Cargo.toml index 6decacb4a980..4e9345f9e6a6 100644 --- a/packages/ic-pub-key/Cargo.toml +++ b/packages/ic-pub-key/Cargo.toml @@ -11,18 +11,20 @@ edition.workspace = true documentation.workspace = true [features] -default = ["secp256k1", "ed25519", "vetkeys"] +default = ["secp256k1", "secp256r1", "ed25519", "vetkeys"] secp256k1 = ["dep:ic-secp256k1"] +secp256r1 = ["dep:ic-secp256r1"] ed25519 = ["dep:ic-ed25519"] vetkeys = ["dep:ic-vetkeys", "dep:ic-cdk-management-canister"] [dependencies] ic-secp256k1 = { path = "../ic-secp256k1", version = "0.3.0", optional = true } +ic-secp256r1 = { path = "../ic-secp256r1", version = "0.2.0", optional = true } ic-ed25519 = { path = "../ic-ed25519", version = "0.6.0", optional = true } ic-vetkeys = { workspace = true, optional = true } ic-cdk-management-canister = { workspace = true, optional = true } -ic-management-canister-types = { workspace = true } +ic-management-canister-types = { path = "../ic-management-canister-types", version = "0.11.0" } [dev-dependencies] hex = { workspace = true } diff --git a/packages/ic-pub-key/src/lib.rs b/packages/ic-pub-key/src/lib.rs index f557cdd4686e..abbbd63b1d51 100644 --- a/packages/ic-pub-key/src/lib.rs +++ b/packages/ic-pub-key/src/lib.rs @@ -27,8 +27,15 @@ //! As an extension of the management canister interface, this crate also allows //! deriving keys relative to the hardcoded test master keys used by PocketIC. -#[cfg(not(any(feature = "secp256k1", feature = "ed25519", feature = "vetkeys")))] -compile_error!("At least one of the features (secp256k1, ed25519, vetkeys) must be enabled"); +#[cfg(not(any( + feature = "secp256k1", + feature = "secp256r1", + feature = "ed25519", + feature = "vetkeys" +)))] +compile_error!( + "At least one of the features (secp256k1, secp256r1, ed25519, vetkeys) must be enabled" +); pub use ic_management_canister_types::{ CanisterId, EcdsaCurve, EcdsaKeyId, EcdsaPublicKeyArgs, EcdsaPublicKeyResult, SchnorrAlgorithm, @@ -59,6 +66,8 @@ enum MasterPublicKeyInner { EcdsaSecp256k1(ic_secp256k1::PublicKey), #[cfg(feature = "secp256k1")] Bip340Secp256k1(ic_secp256k1::PublicKey), + #[cfg(feature = "secp256r1")] + EcdsaSecp256r1(ic_secp256r1::PublicKey), #[cfg(feature = "ed25519")] Ed25519(ic_ed25519::PublicKey), #[cfg(feature = "vetkeys")] @@ -88,6 +97,13 @@ impl MasterPublicKey { )]); DerivedPublicKeyInner::Bip340Secp256k1(mk.derive_subkey(&path)) } + #[cfg(feature = "secp256r1")] + MasterPublicKeyInner::EcdsaSecp256r1(mk) => { + let path = ic_secp256r1::DerivationPath::new(vec![ic_secp256r1::DerivationIndex( + canister_id.as_slice().to_vec(), + )]); + DerivedPublicKeyInner::EcdsaSecp256r1(mk.derive_subkey(&path)) + } #[cfg(feature = "ed25519")] MasterPublicKeyInner::Ed25519(mk) => { let path = ic_ed25519::DerivationPath::new(vec![ic_ed25519::DerivationIndex( @@ -108,6 +124,24 @@ impl TryFrom<&EcdsaKeyId> for MasterPublicKey { type Error = Error; fn try_from(key_id: &EcdsaKeyId) -> Result { + #[cfg(feature = "secp256r1")] + { + if key_id.curve == EcdsaCurve::Secp256r1 { + let mk = match key_id.name.as_ref() { + "key_1" => ic_secp256r1::PublicKey::mainnet_key( + ic_secp256r1::MasterPublicKeyId::EcdsaKey1, + ), + "test_key_1" => ic_secp256r1::PublicKey::mainnet_key( + ic_secp256r1::MasterPublicKeyId::EcdsaTestKey1, + ), + _ => return Err(Error::UnknownKeyIdentifier), + }; + + let inner = MasterPublicKeyInner::EcdsaSecp256r1(mk); + return Ok(Self { inner }); + } + } + if key_id.curve != EcdsaCurve::Secp256k1 { return Err(Error::AlgorithmNotSupported); } @@ -242,6 +276,8 @@ enum DerivedPublicKeyInner { EcdsaSecp256k1((ic_secp256k1::PublicKey, [u8; 32])), #[cfg(feature = "secp256k1")] Bip340Secp256k1((ic_secp256k1::PublicKey, [u8; 32])), + #[cfg(feature = "secp256r1")] + EcdsaSecp256r1((ic_secp256r1::PublicKey, [u8; 32])), #[cfg(feature = "ed25519")] Ed25519((ic_ed25519::PublicKey, [u8; 32])), #[cfg(feature = "vetkeys")] @@ -286,6 +322,15 @@ impl CanisterMasterKey { ck.0.derive_subkey_with_chain_code(&path, &ck.1), ) } + #[cfg(feature = "secp256r1")] + DerivedPublicKeyInner::EcdsaSecp256r1(ck) => { + let path = ic_secp256r1::DerivationPath::new(vec![ic_secp256r1::DerivationIndex( + context.to_vec(), + )]); + DerivedPublicKeyInner::EcdsaSecp256r1( + ck.0.derive_subkey_with_chain_code(&path, &ck.1), + ) + } #[cfg(feature = "ed25519")] DerivedPublicKeyInner::Ed25519(ck) => { let path = ic_ed25519::DerivationPath::new(vec![ic_ed25519::DerivationIndex( @@ -331,6 +376,18 @@ impl CanisterMasterKey { ck.0.derive_subkey_with_chain_code(&path, &ck.1), ) } + #[cfg(feature = "secp256r1")] + DerivedPublicKeyInner::EcdsaSecp256r1(ck) => { + let path = ic_secp256r1::DerivationPath::new( + path.iter() + .cloned() + .map(ic_secp256r1::DerivationIndex) + .collect(), + ); + DerivedPublicKeyInner::EcdsaSecp256r1( + ck.0.derive_subkey_with_chain_code(&path, &ck.1), + ) + } #[cfg(feature = "ed25519")] DerivedPublicKeyInner::Ed25519(ck) => { let path = ic_ed25519::DerivationPath::new( @@ -359,6 +416,8 @@ impl CanisterMasterKey { DerivedPublicKeyInner::EcdsaSecp256k1(ck) => ck.0.serialize_sec1(true), #[cfg(feature = "secp256k1")] DerivedPublicKeyInner::Bip340Secp256k1(ck) => ck.0.serialize_sec1(true), + #[cfg(feature = "secp256r1")] + DerivedPublicKeyInner::EcdsaSecp256r1(ck) => ck.0.serialize_sec1(true), #[cfg(feature = "ed25519")] DerivedPublicKeyInner::Ed25519(ck) => ck.0.serialize_raw().to_vec(), #[cfg(feature = "vetkeys")] @@ -375,6 +434,8 @@ impl CanisterMasterKey { DerivedPublicKeyInner::EcdsaSecp256k1(ck) => Some(ck.1.to_vec()), #[cfg(feature = "secp256k1")] DerivedPublicKeyInner::Bip340Secp256k1(ck) => Some(ck.1.to_vec()), + #[cfg(feature = "secp256r1")] + DerivedPublicKeyInner::EcdsaSecp256r1(ck) => Some(ck.1.to_vec()), #[cfg(feature = "ed25519")] DerivedPublicKeyInner::Ed25519(ck) => Some(ck.1.to_vec()), #[cfg(feature = "vetkeys")] @@ -396,6 +457,8 @@ impl DerivedPublicKey { DerivedPublicKeyInner::EcdsaSecp256k1(ck) => ck.0.serialize_sec1(true), #[cfg(feature = "secp256k1")] DerivedPublicKeyInner::Bip340Secp256k1(ck) => ck.0.serialize_sec1(true), + #[cfg(feature = "secp256r1")] + DerivedPublicKeyInner::EcdsaSecp256r1(ck) => ck.0.serialize_sec1(true), #[cfg(feature = "ed25519")] DerivedPublicKeyInner::Ed25519(ck) => ck.0.serialize_raw().to_vec(), #[cfg(feature = "vetkeys")] @@ -412,6 +475,8 @@ impl DerivedPublicKey { DerivedPublicKeyInner::EcdsaSecp256k1(ck) => Some(ck.1.to_vec()), #[cfg(feature = "secp256k1")] DerivedPublicKeyInner::Bip340Secp256k1(ck) => Some(ck.1.to_vec()), + #[cfg(feature = "secp256r1")] + DerivedPublicKeyInner::EcdsaSecp256r1(ck) => Some(ck.1.to_vec()), #[cfg(feature = "ed25519")] DerivedPublicKeyInner::Ed25519(ck) => Some(ck.1.to_vec()), #[cfg(feature = "vetkeys")] @@ -440,6 +505,8 @@ impl DerivedPublicKey { /// * `pocketic_test_key_1`: The key used by PocketIC when a request to the management canister /// uses `test_key_1`. /// * `dfx_test_key`: PocketIC specific test key +/// +/// For the secp256r1 curve, only `key_1` and `test_key_1` are supported. pub fn derive_ecdsa_key(args: &EcdsaPublicKeyArgs) -> Result { let canister_id = args.canister_id.ok_or(Error::CanisterIdMissing)?; diff --git a/packages/ic-pub-key/tests/tests.rs b/packages/ic-pub-key/tests/tests.rs index 7b0018f44f97..7d8eb19e1cdb 100644 --- a/packages/ic-pub-key/tests/tests.rs +++ b/packages/ic-pub-key/tests/tests.rs @@ -5,6 +5,22 @@ fn test_ecdsa_derivation( canister_id: &'static str, derivation_path: &[&'static str], expected_dpk: &'static str, +) { + test_ecdsa_derivation_with_curve( + EcdsaCurve::Secp256k1, + key_name, + canister_id, + derivation_path, + expected_dpk, + ); +} + +fn test_ecdsa_derivation_with_curve( + curve: EcdsaCurve, + key_name: &'static str, + canister_id: &'static str, + derivation_path: &[&'static str], + expected_dpk: &'static str, ) { use std::str::FromStr; @@ -17,7 +33,7 @@ fn test_ecdsa_derivation( canister_id: Some(ic_pub_key::CanisterId::from_str(canister_id).unwrap()), derivation_path, key_id: EcdsaKeyId { - curve: EcdsaCurve::Secp256k1, + curve, name: key_name.to_string(), }, }; @@ -310,6 +326,48 @@ fn test_ecdsa_secp256k1_derivation_test_key1() { ); } +// The expected keys were computed offline with `ic-secp256r1`, not taken from the +// `ecdsa_public_key` answers of mainnet. +#[cfg(feature = "secp256r1")] +#[test] +fn test_ecdsa_secp256r1_derivation_key1() { + test_ecdsa_derivation_with_curve( + EcdsaCurve::Secp256r1, + "key_1", + "h5jwf-5iaaa-aaaan-qmvoa-cai", + &[ + "Test", + "Derivation", + "For", + "Mainnet", + "ECDSA", + "secp256r1", + "key_1", + ], + "02cda0a98456eaf736d750a6b3885433df81b9a5e76c2ee74e4552d9de1a3b38a5", + ); +} + +#[cfg(feature = "secp256r1")] +#[test] +fn test_ecdsa_secp256r1_derivation_test_key1() { + test_ecdsa_derivation_with_curve( + EcdsaCurve::Secp256r1, + "test_key_1", + "h5jwf-5iaaa-aaaan-qmvoa-cai", + &[ + "Test", + "Derivation", + "For", + "Mainnet", + "ECDSA", + "secp256r1", + "test_key_1", + ], + "038bee03c1155d5b11e08b90fbdebb584c84ba617b916498b9c3d7bdc7a3d32eb8", + ); +} + #[cfg(feature = "secp256k1")] #[test] fn test_ecdsa_secp256k1_derivation_pocketic_key1() { diff --git a/packages/ic-secp256r1/BUILD.bazel b/packages/ic-secp256r1/BUILD.bazel index 00d95cb4d45e..99a2697c8395 100644 --- a/packages/ic-secp256r1/BUILD.bazel +++ b/packages/ic-secp256r1/BUILD.bazel @@ -9,6 +9,7 @@ rust_library( version = "0.2.0", deps = [ # Keep sorted. + "@crate_index//:hex-literal", "@crate_index//:hmac", "@crate_index//:num-bigint", "@crate_index//:p256", diff --git a/packages/ic-secp256r1/CHANGELOG.md b/packages/ic-secp256r1/CHANGELOG.md index de70e3a46bd0..87a8066a471b 100644 --- a/packages/ic-secp256r1/CHANGELOG.md +++ b/packages/ic-secp256r1/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Add `PublicKey::mainnet_key` returning the mainnet master public keys `key_1` and `test_key_1` + ## [0.2.0] - 2026-02-11 ## Added diff --git a/packages/ic-secp256r1/Cargo.toml b/packages/ic-secp256r1/Cargo.toml index 11212c1cd448..6a8c28d87843 100644 --- a/packages/ic-secp256r1/Cargo.toml +++ b/packages/ic-secp256r1/Cargo.toml @@ -13,6 +13,7 @@ documentation.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +hex-literal = { workspace = true } hmac = { workspace = true } num-bigint = { workspace = true } p256 = { workspace = true } diff --git a/packages/ic-secp256r1/src/lib.rs b/packages/ic-secp256r1/src/lib.rs index a626e4a6bef9..4c68d8fc7e4f 100644 --- a/packages/ic-secp256r1/src/lib.rs +++ b/packages/ic-secp256r1/src/lib.rs @@ -5,6 +5,7 @@ //! A crate with handling of ECDSA keys over the secp256r1 curve +use hex_literal::hex; use p256::{ AffinePoint, NistP256, Scalar, elliptic_curve::{ @@ -543,6 +544,15 @@ impl PrivateKey { } } +/// An identifier for the mainnet production key +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum MasterPublicKeyId { + /// The production master key "key_1" for ECDSA + EcdsaKey1, + /// The test master key "test_key_1" for ECDSA + EcdsaTestKey1, +} + /// An ECDSA public key #[derive(Clone, Eq, PartialEq, Debug)] pub struct PublicKey { @@ -550,6 +560,20 @@ pub struct PublicKey { } impl PublicKey { + /// Return the public master keys used in the production mainnet + pub fn mainnet_key(key_id: MasterPublicKeyId) -> Self { + match key_id { + MasterPublicKeyId::EcdsaKey1 => Self::deserialize_sec1(&hex!( + "03225c6e2c86eeb576f99a456692e11dd41e37382d117d4dbdad6dcc29703a2fe6" + )) + .expect("Hardcoded master key was rejected"), + MasterPublicKeyId::EcdsaTestKey1 => Self::deserialize_sec1(&hex!( + "021a3eee9d02e8d9cc32f19ca572615a1fa4712742b3a623228db9289c87e859c5" + )) + .expect("Hardcoded master key was rejected"), + } + } + /// Deserialize a public key stored in SEC1 format /// /// This is just the encoding of the point. Both compressed and uncompressed diff --git a/packages/ic-secp256r1/tests/tests.rs b/packages/ic-secp256r1/tests/tests.rs index 4eb269ff0171..25a7611abd3e 100644 --- a/packages/ic-secp256r1/tests/tests.rs +++ b/packages/ic-secp256r1/tests/tests.rs @@ -535,3 +535,17 @@ fn non_canonical_encodings_are_rejected() { assert!(PublicKey::deserialize_canonical_der(&nce).is_err()); } } + +#[test] +fn mainnet_keys_are_the_ones_in_the_cups() { + // The master public keys of `ecdsa:Secp256r1:key_1` and `ecdsa:Secp256r1:test_key_1` + // in the CUPs of subnets uzr34 and fuqsr. + assert_eq!( + hex::encode(PublicKey::mainnet_key(MasterPublicKeyId::EcdsaKey1).serialize_sec1(true)), + "03225c6e2c86eeb576f99a456692e11dd41e37382d117d4dbdad6dcc29703a2fe6", + ); + assert_eq!( + hex::encode(PublicKey::mainnet_key(MasterPublicKeyId::EcdsaTestKey1).serialize_sec1(true)), + "021a3eee9d02e8d9cc32f19ca572615a1fa4712742b3a623228db9289c87e859c5", + ); +} From 7beb3230c36f4de72a3152e0059246ff60f6e2f5 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Mon, 19 Oct 2026 16:12:13 +0000 Subject: [PATCH 2/7] feat(crypto): support the PocketIC secp256r1 ECDSA keys in ic-pub-key PocketIC holds secp256r1 keys for `key_1`, `test_key_1` and `dfx_test_key`, as it does for the other algorithms `ic-pub-key` supports. * `ic-secp256r1` gains `PublicKey::pocketic_key` with their master public keys, and a test that derives them the way PocketIC does. * `ic-pub-key` derives keys for `pocketic_key_1`, `pocketic_test_key_1` and `dfx_test_key` over secp256r1. * The expected keys of the secp256r1 tests in `ic-pub-key` are what the replica's `derive_threshold_public_key` returns for these master keys, which a one-off check confirmed for the mainnet ones. Co-Authored-By: Claude Opus 5.5 --- packages/ic-pub-key/CHANGELOG.md | 2 +- packages/ic-pub-key/src/lib.rs | 11 ++++- packages/ic-pub-key/tests/tests.rs | 64 +++++++++++++++++++++++++++- packages/ic-secp256r1/CHANGELOG.md | 1 + packages/ic-secp256r1/src/lib.rs | 34 +++++++++++++++ packages/ic-secp256r1/tests/tests.rs | 18 ++++++++ 6 files changed, 125 insertions(+), 5 deletions(-) diff --git a/packages/ic-pub-key/CHANGELOG.md b/packages/ic-pub-key/CHANGELOG.md index fcb95ddf8250..0ffe16bd4080 100644 --- a/packages/ic-pub-key/CHANGELOG.md +++ b/packages/ic-pub-key/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add offline derivation for the mainnet ECDSA keys `key_1` and `test_key_1` over the secp256r1 curve, behind the new (default) `secp256r1` feature. +- Add offline derivation for ECDSA keys over the secp256r1 curve, behind the new (default) `secp256r1` feature. It supports the mainnet keys `key_1` and `test_key_1` as well as the PocketIC keys. ### Changed diff --git a/packages/ic-pub-key/src/lib.rs b/packages/ic-pub-key/src/lib.rs index abbbd63b1d51..e6d1cc5a03c2 100644 --- a/packages/ic-pub-key/src/lib.rs +++ b/packages/ic-pub-key/src/lib.rs @@ -134,6 +134,15 @@ impl TryFrom<&EcdsaKeyId> for MasterPublicKey { "test_key_1" => ic_secp256r1::PublicKey::mainnet_key( ic_secp256r1::MasterPublicKeyId::EcdsaTestKey1, ), + "pocketic_key_1" => ic_secp256r1::PublicKey::pocketic_key( + ic_secp256r1::PocketIcMasterPublicKeyId::EcdsaKey1, + ), + "pocketic_test_key_1" => ic_secp256r1::PublicKey::pocketic_key( + ic_secp256r1::PocketIcMasterPublicKeyId::EcdsaTestKey1, + ), + "dfx_test_key" => ic_secp256r1::PublicKey::pocketic_key( + ic_secp256r1::PocketIcMasterPublicKeyId::EcdsaDfxTestKey, + ), _ => return Err(Error::UnknownKeyIdentifier), }; @@ -505,8 +514,6 @@ impl DerivedPublicKey { /// * `pocketic_test_key_1`: The key used by PocketIC when a request to the management canister /// uses `test_key_1`. /// * `dfx_test_key`: PocketIC specific test key -/// -/// For the secp256r1 curve, only `key_1` and `test_key_1` are supported. pub fn derive_ecdsa_key(args: &EcdsaPublicKeyArgs) -> Result { let canister_id = args.canister_id.ok_or(Error::CanisterIdMissing)?; diff --git a/packages/ic-pub-key/tests/tests.rs b/packages/ic-pub-key/tests/tests.rs index 7d8eb19e1cdb..9a2d2862bcb1 100644 --- a/packages/ic-pub-key/tests/tests.rs +++ b/packages/ic-pub-key/tests/tests.rs @@ -326,8 +326,8 @@ fn test_ecdsa_secp256k1_derivation_test_key1() { ); } -// The expected keys were computed offline with `ic-secp256r1`, not taken from the -// `ecdsa_public_key` answers of mainnet. +// The expected secp256r1 keys are what the replica's `derive_threshold_public_key`, +// which answers `ecdsa_public_key`, returns for these master keys. #[cfg(feature = "secp256r1")] #[test] fn test_ecdsa_secp256r1_derivation_key1() { @@ -425,6 +425,66 @@ fn test_ecdsa_secp256k1_derivation_pocketic_dfx_test_key() { ); } +#[cfg(feature = "secp256r1")] +#[test] +fn test_ecdsa_secp256r1_derivation_pocketic_key1() { + test_ecdsa_derivation_with_curve( + EcdsaCurve::Secp256r1, + "pocketic_key_1", + "uzt4z-lp777-77774-qaabq-cai", + &[ + "Test", + "Derivation", + "For", + "PocketIC", + "ECDSA", + "secp256r1", + "key_1", + ], + "02d3536db48ecef1eb8249f04553046902fbe7d979140530e5294452b94f0e38ad", + ); +} + +#[cfg(feature = "secp256r1")] +#[test] +fn test_ecdsa_secp256r1_derivation_pocketic_test_key1() { + test_ecdsa_derivation_with_curve( + EcdsaCurve::Secp256r1, + "pocketic_test_key_1", + "uzt4z-lp777-77774-qaabq-cai", + &[ + "Test", + "Derivation", + "For", + "PocketIC", + "ECDSA", + "secp256r1", + "test_key_1", + ], + "03080d5850ca18f277d7f39c3cfd37635b6527a73ba63b774d26d4cdf725e1ce39", + ); +} + +#[cfg(feature = "secp256r1")] +#[test] +fn test_ecdsa_secp256r1_derivation_pocketic_dfx_test_key() { + test_ecdsa_derivation_with_curve( + EcdsaCurve::Secp256r1, + "dfx_test_key", + "uzt4z-lp777-77774-qaabq-cai", + &[ + "Test", + "Derivation", + "For", + "PocketIC", + "ECDSA", + "secp256r1", + "dfx_test_key", + ], + "036cd2e52401b60ec8b86041f9c624438eb69ab271def9c9a439bd9fb49fa88ac7", + ); +} + #[test] fn test_vetkd_derivation_using_test_key_1() { // This test data was generated on mainnet using test_key_1 diff --git a/packages/ic-secp256r1/CHANGELOG.md b/packages/ic-secp256r1/CHANGELOG.md index 87a8066a471b..c17e9fc97406 100644 --- a/packages/ic-secp256r1/CHANGELOG.md +++ b/packages/ic-secp256r1/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `PublicKey::mainnet_key` returning the mainnet master public keys `key_1` and `test_key_1` +- Add `PublicKey::pocketic_key` returning the master public keys PocketIC uses for `key_1`, `test_key_1` and `dfx_test_key` ## [0.2.0] - 2026-02-11 diff --git a/packages/ic-secp256r1/src/lib.rs b/packages/ic-secp256r1/src/lib.rs index 4c68d8fc7e4f..27afa3121213 100644 --- a/packages/ic-secp256r1/src/lib.rs +++ b/packages/ic-secp256r1/src/lib.rs @@ -553,6 +553,17 @@ pub enum MasterPublicKeyId { EcdsaTestKey1, } +/// An identifier for the hardcoded keys used by PocketIC +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum PocketIcMasterPublicKeyId { + /// The PocketIC hardcoded key for ECDSA "key_1" + EcdsaKey1, + /// The PocketIC hardcoded key for ECDSA "test_key_1" + EcdsaTestKey1, + /// The PocketIC hardcoded key for ECDSA "dfx_test_key" + EcdsaDfxTestKey, +} + /// An ECDSA public key #[derive(Clone, Eq, PartialEq, Debug)] pub struct PublicKey { @@ -574,6 +585,29 @@ impl PublicKey { } } + /// Return the public master keys used by PocketIC + /// + /// PocketIC derives each of these keys from + /// `PrivateKey::generate_insecure_key_for_testing(42)` and the key name, so + /// their secret keys are known, and they should only be used for offline + /// testing with PocketIC + pub fn pocketic_key(key_id: PocketIcMasterPublicKeyId) -> Self { + match key_id { + PocketIcMasterPublicKeyId::EcdsaKey1 => Self::deserialize_sec1(&hex!( + "0384e7ba2fc1e46ed22e8d7d2b885b16c36270b3403be76e877ae961aec99abc28" + )) + .expect("Hardcoded master key was rejected"), + PocketIcMasterPublicKeyId::EcdsaTestKey1 => Self::deserialize_sec1(&hex!( + "036c7bdaf10cc2bb6709ea76162db3e5f31f77014fc79741b044618f51ba61ee2b" + )) + .expect("Hardcoded master key was rejected"), + PocketIcMasterPublicKeyId::EcdsaDfxTestKey => Self::deserialize_sec1(&hex!( + "0287dd82d1da499aec7d1707e6e23a3522a5c2e544c64b573b1221545803ae1c39" + )) + .expect("Hardcoded master key was rejected"), + } + } + /// Deserialize a public key stored in SEC1 format /// /// This is just the encoding of the point. Both compressed and uncompressed diff --git a/packages/ic-secp256r1/tests/tests.rs b/packages/ic-secp256r1/tests/tests.rs index 25a7611abd3e..5957d4f81745 100644 --- a/packages/ic-secp256r1/tests/tests.rs +++ b/packages/ic-secp256r1/tests/tests.rs @@ -549,3 +549,21 @@ fn mainnet_keys_are_the_ones_in_the_cups() { "021a3eee9d02e8d9cc32f19ca572615a1fa4712742b3a623228db9289c87e859c5", ); } + +#[test] +fn pocketic_keys_are_derived_like_in_pocketic() { + // PocketIC derives each key from this seed and the key name. + let seed_key = PrivateKey::generate_insecure_key_for_testing(42); + for (key_id, name) in [ + (PocketIcMasterPublicKeyId::EcdsaKey1, "key_1"), + (PocketIcMasterPublicKeyId::EcdsaTestKey1, "test_key_1"), + (PocketIcMasterPublicKeyId::EcdsaDfxTestKey, "dfx_test_key"), + ] { + let path = DerivationPath::new(vec![DerivationIndex(name.as_bytes().to_vec())]); + assert_eq!( + PublicKey::pocketic_key(key_id), + seed_key.derive_subkey(&path).0.public_key(), + "{name}", + ); + } +} From 03a72b39c85deb0f20172f14ef9a40ddc7320b5c Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 20 Oct 2026 07:38:21 +0000 Subject: [PATCH 3/7] feat(crypto): add the mainnet and PocketIC master keys to ic-secp256r1 `ic-secp256k1` and `ic-ed25519` ship the mainnet and PocketIC master public keys of their algorithms, which `ic-pub-key` uses for offline key derivation. The mainnet ECDSA keys `key_1` and `test_key_1` now also exist over secp256r1, and PocketIC holds secp256r1 keys too, but `ic-secp256r1` has none of them. * `PublicKey::mainnet_key` returns the master public keys of `ecdsa:Secp256r1:key_1` and `ecdsa:Secp256r1:test_key_1`, read from the CUPs of subnets uzr34 and fuqsr. * `PublicKey::pocketic_key` returns the ones PocketIC uses for `key_1`, `test_key_1` and `dfx_test_key`, and a test derives them the way PocketIC does. * The version moves to 0.3.0, to be released before `ic-pub-key` can depend on these keys. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- packages/ic-secp256r1/BUILD.bazel | 3 +- packages/ic-secp256r1/CHANGELOG.md | 7 ++++ packages/ic-secp256r1/Cargo.toml | 3 +- packages/ic-secp256r1/src/lib.rs | 58 ++++++++++++++++++++++++++++ packages/ic-secp256r1/tests/tests.rs | 32 +++++++++++++++ 6 files changed, 102 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1a1874d8858e..e4099871f176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13578,7 +13578,7 @@ dependencies = [ [[package]] name = "ic-secp256r1" -version = "0.2.0" +version = "0.3.0" dependencies = [ "hex", "hex-literal 1.1.0", diff --git a/packages/ic-secp256r1/BUILD.bazel b/packages/ic-secp256r1/BUILD.bazel index 00d95cb4d45e..98f26db56398 100644 --- a/packages/ic-secp256r1/BUILD.bazel +++ b/packages/ic-secp256r1/BUILD.bazel @@ -6,9 +6,10 @@ rust_library( name = "ic-secp256r1", srcs = glob(["src/**"]), crate_name = "ic_secp256r1", - version = "0.2.0", + version = "0.3.0", deps = [ # Keep sorted. + "@crate_index//:hex-literal", "@crate_index//:hmac", "@crate_index//:num-bigint", "@crate_index//:p256", diff --git a/packages/ic-secp256r1/CHANGELOG.md b/packages/ic-secp256r1/CHANGELOG.md index de70e3a46bd0..b4bb32f54840 100644 --- a/packages/ic-secp256r1/CHANGELOG.md +++ b/packages/ic-secp256r1/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.3.0] - 2026-10-20 + +### Added + +- Add `PublicKey::mainnet_key` returning the mainnet master public keys `key_1` and `test_key_1` +- Add `PublicKey::pocketic_key` returning the master public keys PocketIC uses for `key_1`, `test_key_1` and `dfx_test_key` + ## [0.2.0] - 2026-02-11 ## Added diff --git a/packages/ic-secp256r1/Cargo.toml b/packages/ic-secp256r1/Cargo.toml index 11212c1cd448..3d0eb9a31db0 100644 --- a/packages/ic-secp256r1/Cargo.toml +++ b/packages/ic-secp256r1/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ic-secp256r1" -version = "0.2.0" +version = "0.3.0" description = "A package created for the Internet Computer Protocol for the handling of ECDSA keys over the secp256r1 curve." license = "Apache-2.0" readme = "README.md" @@ -13,6 +13,7 @@ documentation.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +hex-literal = { workspace = true } hmac = { workspace = true } num-bigint = { workspace = true } p256 = { workspace = true } diff --git a/packages/ic-secp256r1/src/lib.rs b/packages/ic-secp256r1/src/lib.rs index a626e4a6bef9..27afa3121213 100644 --- a/packages/ic-secp256r1/src/lib.rs +++ b/packages/ic-secp256r1/src/lib.rs @@ -5,6 +5,7 @@ //! A crate with handling of ECDSA keys over the secp256r1 curve +use hex_literal::hex; use p256::{ AffinePoint, NistP256, Scalar, elliptic_curve::{ @@ -543,6 +544,26 @@ impl PrivateKey { } } +/// An identifier for the mainnet production key +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum MasterPublicKeyId { + /// The production master key "key_1" for ECDSA + EcdsaKey1, + /// The test master key "test_key_1" for ECDSA + EcdsaTestKey1, +} + +/// An identifier for the hardcoded keys used by PocketIC +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum PocketIcMasterPublicKeyId { + /// The PocketIC hardcoded key for ECDSA "key_1" + EcdsaKey1, + /// The PocketIC hardcoded key for ECDSA "test_key_1" + EcdsaTestKey1, + /// The PocketIC hardcoded key for ECDSA "dfx_test_key" + EcdsaDfxTestKey, +} + /// An ECDSA public key #[derive(Clone, Eq, PartialEq, Debug)] pub struct PublicKey { @@ -550,6 +571,43 @@ pub struct PublicKey { } impl PublicKey { + /// Return the public master keys used in the production mainnet + pub fn mainnet_key(key_id: MasterPublicKeyId) -> Self { + match key_id { + MasterPublicKeyId::EcdsaKey1 => Self::deserialize_sec1(&hex!( + "03225c6e2c86eeb576f99a456692e11dd41e37382d117d4dbdad6dcc29703a2fe6" + )) + .expect("Hardcoded master key was rejected"), + MasterPublicKeyId::EcdsaTestKey1 => Self::deserialize_sec1(&hex!( + "021a3eee9d02e8d9cc32f19ca572615a1fa4712742b3a623228db9289c87e859c5" + )) + .expect("Hardcoded master key was rejected"), + } + } + + /// Return the public master keys used by PocketIC + /// + /// PocketIC derives each of these keys from + /// `PrivateKey::generate_insecure_key_for_testing(42)` and the key name, so + /// their secret keys are known, and they should only be used for offline + /// testing with PocketIC + pub fn pocketic_key(key_id: PocketIcMasterPublicKeyId) -> Self { + match key_id { + PocketIcMasterPublicKeyId::EcdsaKey1 => Self::deserialize_sec1(&hex!( + "0384e7ba2fc1e46ed22e8d7d2b885b16c36270b3403be76e877ae961aec99abc28" + )) + .expect("Hardcoded master key was rejected"), + PocketIcMasterPublicKeyId::EcdsaTestKey1 => Self::deserialize_sec1(&hex!( + "036c7bdaf10cc2bb6709ea76162db3e5f31f77014fc79741b044618f51ba61ee2b" + )) + .expect("Hardcoded master key was rejected"), + PocketIcMasterPublicKeyId::EcdsaDfxTestKey => Self::deserialize_sec1(&hex!( + "0287dd82d1da499aec7d1707e6e23a3522a5c2e544c64b573b1221545803ae1c39" + )) + .expect("Hardcoded master key was rejected"), + } + } + /// Deserialize a public key stored in SEC1 format /// /// This is just the encoding of the point. Both compressed and uncompressed diff --git a/packages/ic-secp256r1/tests/tests.rs b/packages/ic-secp256r1/tests/tests.rs index 4eb269ff0171..5957d4f81745 100644 --- a/packages/ic-secp256r1/tests/tests.rs +++ b/packages/ic-secp256r1/tests/tests.rs @@ -535,3 +535,35 @@ fn non_canonical_encodings_are_rejected() { assert!(PublicKey::deserialize_canonical_der(&nce).is_err()); } } + +#[test] +fn mainnet_keys_are_the_ones_in_the_cups() { + // The master public keys of `ecdsa:Secp256r1:key_1` and `ecdsa:Secp256r1:test_key_1` + // in the CUPs of subnets uzr34 and fuqsr. + assert_eq!( + hex::encode(PublicKey::mainnet_key(MasterPublicKeyId::EcdsaKey1).serialize_sec1(true)), + "03225c6e2c86eeb576f99a456692e11dd41e37382d117d4dbdad6dcc29703a2fe6", + ); + assert_eq!( + hex::encode(PublicKey::mainnet_key(MasterPublicKeyId::EcdsaTestKey1).serialize_sec1(true)), + "021a3eee9d02e8d9cc32f19ca572615a1fa4712742b3a623228db9289c87e859c5", + ); +} + +#[test] +fn pocketic_keys_are_derived_like_in_pocketic() { + // PocketIC derives each key from this seed and the key name. + let seed_key = PrivateKey::generate_insecure_key_for_testing(42); + for (key_id, name) in [ + (PocketIcMasterPublicKeyId::EcdsaKey1, "key_1"), + (PocketIcMasterPublicKeyId::EcdsaTestKey1, "test_key_1"), + (PocketIcMasterPublicKeyId::EcdsaDfxTestKey, "dfx_test_key"), + ] { + let path = DerivationPath::new(vec![DerivationIndex(name.as_bytes().to_vec())]); + assert_eq!( + PublicKey::pocketic_key(key_id), + seed_key.derive_subkey(&path).0.public_key(), + "{name}", + ); + } +} From 1f5c8cb449f5e8b914a853b15303f6af3c9776a6 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 20 Oct 2026 12:03:18 +0000 Subject: [PATCH 4/7] fix(crypto): address review comments on the ic-secp256r1 master keys * Add `PublicKey::derive_mainnet_key` and `PublicKey::derive_pocketic_key`, and re-export `ic_principal::Principal` as `CanisterId` for them, as `ic-secp256k1` and `ic-ed25519` do. Their tests expect what the replica's `derive_threshold_public_key` returns for the same master keys, canisters and paths. * Test the SLIP-10 retry for nist256p1 (index 33941), the only branch of the derivation the tests didn't reach. It runs when iL >= n, about once in 2^32 steps on P-256. * Rename `mainnet_keys_are_the_ones_in_the_cups` to `mainnet_keys_round_trip`, which is what it checks. * Name the two new enums in the changelog, drop the duplicate `hex-literal` dev-dependency, and list `ic-secp256r1` among the crates with hardcoded master keys in the `cup_explorer` README. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 + packages/ic-secp256r1/BUILD.bazel | 3 + packages/ic-secp256r1/CHANGELOG.md | 5 +- packages/ic-secp256r1/Cargo.toml | 3 +- packages/ic-secp256r1/src/lib.rs | 33 +++++ packages/ic-secp256r1/tests/tests.rs | 183 ++++++++++++++++++++++++++- rs/cup_explorer/README.md | 2 +- 7 files changed, 225 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4099871f176..25a00ba10666 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13583,6 +13583,7 @@ dependencies = [ "hex", "hex-literal 1.1.0", "hmac", + "ic_principal", "num-bigint 0.4.6", "p256", "pem 3.0.6", diff --git a/packages/ic-secp256r1/BUILD.bazel b/packages/ic-secp256r1/BUILD.bazel index 98f26db56398..7115fd328469 100644 --- a/packages/ic-secp256r1/BUILD.bazel +++ b/packages/ic-secp256r1/BUILD.bazel @@ -11,6 +11,7 @@ rust_library( # Keep sorted. "@crate_index//:hex-literal", "@crate_index//:hmac", + "@crate_index//:ic_principal", "@crate_index//:num-bigint", "@crate_index//:p256", "@crate_index//:pem", @@ -32,6 +33,7 @@ rust_test( "@crate_index//:hex", "@crate_index//:hex-literal", "@crate_index//:hmac", + "@crate_index//:ic_principal", "@crate_index//:num-bigint", "@crate_index//:p256", "@crate_index//:pem", @@ -55,6 +57,7 @@ rust_test_suite( "@crate_index//:hex", "@crate_index//:hex-literal", "@crate_index//:hmac", + "@crate_index//:ic_principal", "@crate_index//:num-bigint", "@crate_index//:p256", "@crate_index//:pem", diff --git a/packages/ic-secp256r1/CHANGELOG.md b/packages/ic-secp256r1/CHANGELOG.md index b4bb32f54840..1d9fb837720a 100644 --- a/packages/ic-secp256r1/CHANGELOG.md +++ b/packages/ic-secp256r1/CHANGELOG.md @@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `PublicKey::mainnet_key` returning the mainnet master public keys `key_1` and `test_key_1` -- Add `PublicKey::pocketic_key` returning the master public keys PocketIC uses for `key_1`, `test_key_1` and `dfx_test_key` +- Add `PublicKey::mainnet_key` returning the mainnet master public keys `key_1` and `test_key_1`, identified by the new `MasterPublicKeyId` +- Add `PublicKey::pocketic_key` returning the master public keys PocketIC uses for `key_1`, `test_key_1` and `dfx_test_key`, identified by the new `PocketIcMasterPublicKeyId` +- Add `PublicKey::derive_mainnet_key` and `PublicKey::derive_pocketic_key`, offline equivalents of the `ecdsa_public_key` management canister call, and re-export `ic_principal::Principal` as `CanisterId` for them ## [0.2.0] - 2026-02-11 diff --git a/packages/ic-secp256r1/Cargo.toml b/packages/ic-secp256r1/Cargo.toml index 3d0eb9a31db0..5800d8eeb4d2 100644 --- a/packages/ic-secp256r1/Cargo.toml +++ b/packages/ic-secp256r1/Cargo.toml @@ -15,6 +15,7 @@ documentation.workspace = true [dependencies] hex-literal = { workspace = true } hmac = { workspace = true } +ic_principal = { workspace = true } num-bigint = { workspace = true } p256 = { workspace = true } pem = { workspace = true } @@ -26,6 +27,6 @@ zeroize = { workspace = true } [dev-dependencies] hex = { workspace = true } -hex-literal = { workspace = true } +ic_principal = { workspace = true, features = ["convert"] } rand_chacha = { workspace = true } wycheproof = { workspace = true, features = ["ecdsa"] } diff --git a/packages/ic-secp256r1/src/lib.rs b/packages/ic-secp256r1/src/lib.rs index 27afa3121213..e376bc4c657b 100644 --- a/packages/ic-secp256r1/src/lib.rs +++ b/packages/ic-secp256r1/src/lib.rs @@ -17,6 +17,8 @@ use rand::{CryptoRng, RngCore}; use std::sync::LazyLock; use zeroize::ZeroizeOnDrop; +pub use ic_principal::Principal as CanisterId; + /// An error indicating that decoding a key failed #[derive(Clone, Debug)] pub enum KeyDecodingError { @@ -608,6 +610,37 @@ impl PublicKey { } } + /// Derive a public key from the mainnet parameters + /// + /// This is an offline equivalent to the `ecdsa_public_key` management canister call + pub fn derive_mainnet_key( + key_id: MasterPublicKeyId, + canister_id: &CanisterId, + derivation_path: &[Vec], + ) -> (Self, [u8; 32]) { + let mk = PublicKey::mainnet_key(key_id); + mk.derive_subkey(&DerivationPath::from_canister_id_and_path( + canister_id.as_slice(), + derivation_path, + )) + } + + /// Derive a public key as is done on PocketIC + /// + /// This is an offline equivalent to the `ecdsa_public_key` management canister call + /// when running on PocketIC + pub fn derive_pocketic_key( + key_id: PocketIcMasterPublicKeyId, + canister_id: &CanisterId, + derivation_path: &[Vec], + ) -> (Self, [u8; 32]) { + let mk = PublicKey::pocketic_key(key_id); + mk.derive_subkey(&DerivationPath::from_canister_id_and_path( + canister_id.as_slice(), + derivation_path, + )) + } + /// Deserialize a public key stored in SEC1 format /// /// This is just the encoding of the point. Both compressed and uncompressed diff --git a/packages/ic-secp256r1/tests/tests.rs b/packages/ic-secp256r1/tests/tests.rs index 5957d4f81745..1ae4966a57e1 100644 --- a/packages/ic-secp256r1/tests/tests.rs +++ b/packages/ic-secp256r1/tests/tests.rs @@ -405,6 +405,54 @@ fn should_match_slip10_derivation_test_data() { ); } +#[test] +fn should_match_slip10_derivation_retry_test_data() { + // Test data from https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-derivation-retry-for-nist256p1 + // Deriving index 33941 from m/28578' gives iL >= n, which forces the retry. + let chain_code = hex!("e94c8ebe30c2250a14713212f6449b20f3329105ea15b652ca5bdfc68f6c65c2"); + + let private_key = PrivateKey::deserialize_sec1(&hex!( + "06f0db126f023755d0b8d86d4591718a5210dd8d024e3e14b6159d63f53aa669" + )) + .expect("Test has valid key"); + + let public_key = PublicKey::deserialize_sec1(&hex!( + "02519b5554a4872e8c9c1c847115363051ec43e93400e030ba3c36b52a3e70a5b7" + )) + .expect("Test has valid key"); + + assert_eq!( + hex::encode(public_key.serialize_sec1(true)), + hex::encode(private_key.public_key().serialize_sec1(true)) + ); + + let path = DerivationPath::new_bip32(&[33941]); + + let (derived_secret_key, sk_chain_code) = + private_key.derive_subkey_with_chain_code(&path, &chain_code); + + let (derived_public_key, pk_chain_code) = + public_key.derive_subkey_with_chain_code(&path, &chain_code); + assert_eq!( + hex::encode(sk_chain_code), + "9e87fe95031f14736774cd82f25fd885065cb7c358c1edf813c72af535e83071", + ); + assert_eq!( + hex::encode(pk_chain_code), + "9e87fe95031f14736774cd82f25fd885065cb7c358c1edf813c72af535e83071", + ); + + assert_eq!( + hex::encode(derived_public_key.serialize_sec1(true)), + "0235bfee614c0d5b2cae260000bb1d0d84b270099ad790022c1ae0b2e782efe120", + ); + + assert_eq!( + hex::encode(derived_secret_key.serialize_sec1()), + "092154eed4af83e078ff9b84322015aefe5769e31270f62c3f66c33888335f3a", + ); +} + #[test] fn private_derivation_also_works_for_derived_keys() { let rng = &mut test_rng(); @@ -537,7 +585,7 @@ fn non_canonical_encodings_are_rejected() { } #[test] -fn mainnet_keys_are_the_ones_in_the_cups() { +fn mainnet_keys_round_trip() { // The master public keys of `ecdsa:Secp256r1:key_1` and `ecdsa:Secp256r1:test_key_1` // in the CUPs of subnets uzr34 and fuqsr. assert_eq!( @@ -567,3 +615,136 @@ fn pocketic_keys_are_derived_like_in_pocketic() { ); } } + +fn derivation_path(parts: &[&str]) -> Vec> { + parts.iter().map(|part| part.as_bytes().to_vec()).collect() +} + +// The expected keys in the following tests are what the replica's +// `derive_threshold_public_key`, which answers `ecdsa_public_key`, returns for +// the same master key, canister and derivation path. + +#[test] +fn offline_key_derivation_matches_mainnet_for_key_1() { + use std::str::FromStr; + + let canister_id = CanisterId::from_str("h5jwf-5iaaa-aaaan-qmvoa-cai").unwrap(); + let dpk = PublicKey::derive_mainnet_key( + MasterPublicKeyId::EcdsaKey1, + &canister_id, + &derivation_path(&[ + "Test", + "Derivation", + "For", + "Mainnet", + "ECDSA", + "secp256r1", + "key_1", + ]), + ); + + assert_eq!( + hex::encode(dpk.0.serialize_sec1(true)), + "02cda0a98456eaf736d750a6b3885433df81b9a5e76c2ee74e4552d9de1a3b38a5", + ); +} + +#[test] +fn offline_key_derivation_matches_mainnet_for_test_key_1() { + use std::str::FromStr; + + let canister_id = CanisterId::from_str("h5jwf-5iaaa-aaaan-qmvoa-cai").unwrap(); + let dpk = PublicKey::derive_mainnet_key( + MasterPublicKeyId::EcdsaTestKey1, + &canister_id, + &derivation_path(&[ + "Test", + "Derivation", + "For", + "Mainnet", + "ECDSA", + "secp256r1", + "test_key_1", + ]), + ); + + assert_eq!( + hex::encode(dpk.0.serialize_sec1(true)), + "038bee03c1155d5b11e08b90fbdebb584c84ba617b916498b9c3d7bdc7a3d32eb8", + ); +} + +#[test] +fn offline_key_derivation_matches_pocketic_for_key_1() { + use std::str::FromStr; + + let canister_id = CanisterId::from_str("uzt4z-lp777-77774-qaabq-cai").unwrap(); + let dpk = PublicKey::derive_pocketic_key( + PocketIcMasterPublicKeyId::EcdsaKey1, + &canister_id, + &derivation_path(&[ + "Test", + "Derivation", + "For", + "PocketIC", + "ECDSA", + "secp256r1", + "key_1", + ]), + ); + + assert_eq!( + hex::encode(dpk.0.serialize_sec1(true)), + "02d3536db48ecef1eb8249f04553046902fbe7d979140530e5294452b94f0e38ad", + ); +} + +#[test] +fn offline_key_derivation_matches_pocketic_for_test_key_1() { + use std::str::FromStr; + + let canister_id = CanisterId::from_str("uzt4z-lp777-77774-qaabq-cai").unwrap(); + let dpk = PublicKey::derive_pocketic_key( + PocketIcMasterPublicKeyId::EcdsaTestKey1, + &canister_id, + &derivation_path(&[ + "Test", + "Derivation", + "For", + "PocketIC", + "ECDSA", + "secp256r1", + "test_key_1", + ]), + ); + + assert_eq!( + hex::encode(dpk.0.serialize_sec1(true)), + "03080d5850ca18f277d7f39c3cfd37635b6527a73ba63b774d26d4cdf725e1ce39", + ); +} + +#[test] +fn offline_key_derivation_matches_pocketic_for_dfx_test_key() { + use std::str::FromStr; + + let canister_id = CanisterId::from_str("uzt4z-lp777-77774-qaabq-cai").unwrap(); + let dpk = PublicKey::derive_pocketic_key( + PocketIcMasterPublicKeyId::EcdsaDfxTestKey, + &canister_id, + &derivation_path(&[ + "Test", + "Derivation", + "For", + "PocketIC", + "ECDSA", + "secp256r1", + "dfx_test_key", + ]), + ); + + assert_eq!( + hex::encode(dpk.0.serialize_sec1(true)), + "036cd2e52401b60ec8b86041f9c624438eb69ab271def9c9a439bd9fb49fa88ac7", + ); +} diff --git a/rs/cup_explorer/README.md b/rs/cup_explorer/README.md index f440e4618861..b402af5c2303 100644 --- a/rs/cup_explorer/README.md +++ b/rs/cup_explorer/README.md @@ -66,7 +66,7 @@ bazel run rs/cup_explorer:cup_explorer_bin -- extract-master-public-keys --cup-p - `--cup-path`: Path to a local CUP protobuf file (required). The latest CUP of a subnet can be downloaded using `explore --download-path`. -The public keys are printed hex-encoded, in the encoding of the mainnet master public keys that are hardcoded for [`ic-pub-key`](../../packages/ic-pub-key) (in `ic-secp256k1`, `ic-ed25519` and `ic-vetkeys`): +The public keys are printed hex-encoded, in the encoding of the mainnet master public keys that are hardcoded for [`ic-pub-key`](../../packages/ic-pub-key) (in `ic-secp256k1`, `ic-secp256r1`, `ic-ed25519` and `ic-vetkeys`): - ECDSA and BIP340 Schnorr keys as compressed SEC1 points (33 bytes), - Ed25519 keys as compressed Edwards points (32 bytes), - vetKD keys as compressed BLS12-381 G2 points (96 bytes). From ad372939f28ca6b4fdff6d9b527f6c991ab4f437 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Tue, 20 Oct 2026 12:03:19 +0000 Subject: [PATCH 5/7] fix(crypto): address review findings in ic-pub-key * `TryFrom<&EcdsaKeyId>` has one feature-gated block per curve, like `TryFrom<&SchnorrKeyId>`, instead of depending on the order of its checks. * `serialize` and `chain_code` of `CanisterMasterKey` and `DerivedPublicKey` share one implementation on `DerivedPublicKeyInner`. * The ECDSA tests also check the chain code, and that unknown key names are rejected. All expected values are what the replica's `derive_threshold_public_key` returns. The test helpers follow the Schnorr ones and are feature-gated, so single-feature builds have no unused code. * The crate docs and README describe the features and `default-features = false`, and `derive_ecdsa_key` names the feature each curve needs. * The changelog describes the dependency changes since the published 0.3.0, and `Cargo.toml` explains why `ic-management-canister-types` is the in-repo crate. * The BUILD file lists the features once. Co-Authored-By: Claude Opus 5.5 --- packages/ic-pub-key/BUILD.bazel | 42 ++----- packages/ic-pub-key/CHANGELOG.md | 4 +- packages/ic-pub-key/Cargo.toml | 3 + packages/ic-pub-key/README.md | 8 ++ packages/ic-pub-key/src/lib.rs | 173 +++++++++++++++-------------- packages/ic-pub-key/tests/tests.rs | 126 +++++++++++++++------ 6 files changed, 211 insertions(+), 145 deletions(-) diff --git a/packages/ic-pub-key/BUILD.bazel b/packages/ic-pub-key/BUILD.bazel index b86f66609850..4d2550e505bb 100644 --- a/packages/ic-pub-key/BUILD.bazel +++ b/packages/ic-pub-key/BUILD.bazel @@ -2,15 +2,17 @@ load("@rules_rust//rust:defs.bzl", "rust_doc", "rust_doc_test", "rust_library", package(default_visibility = ["//visibility:public"]) +FEATURES = [ + "secp256k1", + "secp256r1", + "ed25519", + "vetkeys", +] + rust_library( name = "ic-pub-key", srcs = glob(["src/**/*.rs"]), - crate_features = [ - "secp256k1", - "secp256r1", - "ed25519", - "vetkeys", - ], + crate_features = FEATURES, crate_name = "ic_pub_key", version = "0.3.0", deps = [ @@ -29,46 +31,26 @@ rust_library( rust_doc( name = "doc", crate = ":ic-pub-key", - rustdoc_flags = [ - "--cfg=feature=\"secp256k1\"", - "--cfg=feature=\"secp256r1\"", - "--cfg=feature=\"ed25519\"", - "--cfg=feature=\"vetkeys\"", - ], + rustdoc_flags = ["--cfg=feature=\"%s\"" % feature for feature in FEATURES], ) rust_doc_test( name = "doc_test", crate = ":ic-pub-key", - crate_features = [ - "secp256k1", - "secp256r1", - "ed25519", - "vetkeys", - ], + crate_features = FEATURES, deps = [":ic-pub-key"], ) rust_test( name = "unit_tests", crate = ":ic-pub-key", - crate_features = [ - "secp256k1", - "secp256r1", - "ed25519", - "vetkeys", - ], + crate_features = FEATURES, ) rust_test_suite( name = "integration_tests", srcs = glob(["tests/**/*.rs"]), - crate_features = [ - "secp256k1", - "secp256r1", - "ed25519", - "vetkeys", - ], + crate_features = FEATURES, deps = [ ":ic-pub-key", "@crate_index//:hex", diff --git a/packages/ic-pub-key/CHANGELOG.md b/packages/ic-pub-key/CHANGELOG.md index 0ffe16bd4080..6a0fe921c6b9 100644 --- a/packages/ic-pub-key/CHANGELOG.md +++ b/packages/ic-pub-key/CHANGELOG.md @@ -13,7 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed -- Upgrade the `ic-management-canister-types` dependency from `0.8.0` to `0.11.0`, whose `EcdsaCurve` has the `Secp256r1` variant. The re-exported types now come from that version. +- Upgrade the `ic-management-canister-types` dependency from `0.5.0` to `0.11.0`, whose `EcdsaCurve` has the `Secp256r1` variant. The re-exported types now come from that version. +- Upgrade the (optional) `ic-vetkeys` dependency from `0.6.0` to `0.7.0`. +- The `vetkeys` feature now also depends on `ic-cdk-management-canister`. ## [0.3.0] - 2026-02-04 diff --git a/packages/ic-pub-key/Cargo.toml b/packages/ic-pub-key/Cargo.toml index 46c35a9741c9..c1f18263b46f 100644 --- a/packages/ic-pub-key/Cargo.toml +++ b/packages/ic-pub-key/Cargo.toml @@ -24,6 +24,9 @@ ic-secp256r1 = { path = "../ic-secp256r1", version = "0.3.0", optional = true } ic-ed25519 = { path = "../ic-ed25519", version = "0.6.0", optional = true } ic-vetkeys = { workspace = true, optional = true } ic-cdk-management-canister = { workspace = true, optional = true } +# The in-repo crate, since the workspace's version has no `EcdsaCurve::Secp256r1`. +# This crate re-exports its types, so each breaking release of it also needs a +# breaking release of this crate. ic-management-canister-types = { path = "../ic-management-canister-types", version = "0.11.0" } [dev-dependencies] diff --git a/packages/ic-pub-key/README.md b/packages/ic-pub-key/README.md index b0d35e3764d8..fe1674afa2b3 100644 --- a/packages/ic-pub-key/README.md +++ b/packages/ic-pub-key/README.md @@ -9,3 +9,11 @@ context data. This derivation can be performed "online" by a canister using a query call, but this is inconvenient in some usage scenarios. This crate offers an alternative, namely fully *offline* key derivation. + +### Features + +Each kind of key needs its feature, and all of them are enabled by default: +`secp256k1` (ECDSA over secp256k1 and BIP340 Schnorr), `secp256r1` (ECDSA over +secp256r1), `ed25519` (Ed25519 Schnorr) and `vetkeys` (VetKD). To build only some +of them, for example to keep a canister's Wasm small, depend on this crate with +`default-features = false` and enable the features you need. diff --git a/packages/ic-pub-key/src/lib.rs b/packages/ic-pub-key/src/lib.rs index e6d1cc5a03c2..ee0636e302d6 100644 --- a/packages/ic-pub-key/src/lib.rs +++ b/packages/ic-pub-key/src/lib.rs @@ -26,6 +26,19 @@ //! //! As an extension of the management canister interface, this crate also allows //! deriving keys relative to the hardcoded test master keys used by PocketIC. +//! +//! # Features +//! +//! Each kind of key needs its feature, and all of them are enabled by default: +//! +//! * `secp256k1`: ECDSA over secp256k1 and BIP340 Schnorr +//! * `secp256r1`: ECDSA over secp256r1 (P-256) +//! * `ed25519`: Ed25519 Schnorr +//! * `vetkeys`: VetKD +//! +//! Deriving a key whose feature is disabled fails with [`Error::AlgorithmNotSupported`]. +//! To build only some of them, for example to keep a canister's Wasm small, depend on +//! this crate with `default-features = false` and enable the features you need. #[cfg(not(any( feature = "secp256k1", @@ -123,7 +136,38 @@ impl MasterPublicKey { impl TryFrom<&EcdsaKeyId> for MasterPublicKey { type Error = Error; + #[cfg_attr( + not(any(feature = "secp256k1", feature = "secp256r1")), + allow(unused_variables) + )] fn try_from(key_id: &EcdsaKeyId) -> Result { + #[cfg(feature = "secp256k1")] + { + if key_id.curve == EcdsaCurve::Secp256k1 { + let mk = match key_id.name.as_ref() { + "key_1" => ic_secp256k1::PublicKey::mainnet_key( + ic_secp256k1::MasterPublicKeyId::EcdsaKey1, + ), + "test_key_1" => ic_secp256k1::PublicKey::mainnet_key( + ic_secp256k1::MasterPublicKeyId::EcdsaTestKey1, + ), + "pocketic_key_1" => ic_secp256k1::PublicKey::pocketic_key( + ic_secp256k1::PocketIcMasterPublicKeyId::EcdsaKey1, + ), + "pocketic_test_key_1" => ic_secp256k1::PublicKey::pocketic_key( + ic_secp256k1::PocketIcMasterPublicKeyId::EcdsaTestKey1, + ), + "dfx_test_key" => ic_secp256k1::PublicKey::pocketic_key( + ic_secp256k1::PocketIcMasterPublicKeyId::EcdsaDfxTestKey, + ), + _ => return Err(Error::UnknownKeyIdentifier), + }; + + let inner = MasterPublicKeyInner::EcdsaSecp256k1(mk); + return Ok(Self { inner }); + } + } + #[cfg(feature = "secp256r1")] { if key_id.curve == EcdsaCurve::Secp256r1 { @@ -151,47 +195,17 @@ impl TryFrom<&EcdsaKeyId> for MasterPublicKey { } } - if key_id.curve != EcdsaCurve::Secp256k1 { - return Err(Error::AlgorithmNotSupported); - } - - #[cfg(feature = "secp256k1")] - { - let mk = match (key_id.curve, key_id.name.as_ref()) { - (EcdsaCurve::Secp256k1, "key_1") => { - ic_secp256k1::PublicKey::mainnet_key(ic_secp256k1::MasterPublicKeyId::EcdsaKey1) - } - (EcdsaCurve::Secp256k1, "test_key_1") => ic_secp256k1::PublicKey::mainnet_key( - ic_secp256k1::MasterPublicKeyId::EcdsaTestKey1, - ), - (EcdsaCurve::Secp256k1, "pocketic_key_1") => ic_secp256k1::PublicKey::pocketic_key( - ic_secp256k1::PocketIcMasterPublicKeyId::EcdsaKey1, - ), - (EcdsaCurve::Secp256k1, "pocketic_test_key_1") => { - ic_secp256k1::PublicKey::pocketic_key( - ic_secp256k1::PocketIcMasterPublicKeyId::EcdsaTestKey1, - ) - } - (EcdsaCurve::Secp256k1, "dfx_test_key") => ic_secp256k1::PublicKey::pocketic_key( - ic_secp256k1::PocketIcMasterPublicKeyId::EcdsaDfxTestKey, - ), - (_, _) => return Err(Error::UnknownKeyIdentifier), - }; - - let inner = MasterPublicKeyInner::EcdsaSecp256k1(mk); - Ok(Self { inner }) - } - - #[cfg(not(feature = "secp256k1"))] - { - Err(Error::AlgorithmNotSupported) - } + Err(Error::AlgorithmNotSupported) } } impl TryFrom<&SchnorrKeyId> for MasterPublicKey { type Error = Error; + #[cfg_attr( + not(any(feature = "secp256k1", feature = "ed25519")), + allow(unused_variables) + )] fn try_from(key_id: &SchnorrKeyId) -> Result { #[cfg(feature = "secp256k1")] { @@ -254,6 +268,7 @@ impl TryFrom<&SchnorrKeyId> for MasterPublicKey { impl TryFrom<&VetKDKeyId> for MasterPublicKey { type Error = Error; + #[cfg_attr(not(feature = "vetkeys"), allow(unused_variables))] fn try_from(key_id: &VetKDKeyId) -> Result { #[cfg(feature = "vetkeys")] { @@ -293,6 +308,38 @@ enum DerivedPublicKeyInner { VetKD(ic_vetkeys::DerivedPublicKey), } +impl DerivedPublicKeyInner { + fn serialize(&self) -> Vec { + match self { + #[cfg(feature = "secp256k1")] + Self::EcdsaSecp256k1(ck) => ck.0.serialize_sec1(true), + #[cfg(feature = "secp256k1")] + Self::Bip340Secp256k1(ck) => ck.0.serialize_sec1(true), + #[cfg(feature = "secp256r1")] + Self::EcdsaSecp256r1(ck) => ck.0.serialize_sec1(true), + #[cfg(feature = "ed25519")] + Self::Ed25519(ck) => ck.0.serialize_raw().to_vec(), + #[cfg(feature = "vetkeys")] + Self::VetKD(ck) => ck.serialize(), + } + } + + fn chain_code(&self) -> Option> { + match self { + #[cfg(feature = "secp256k1")] + Self::EcdsaSecp256k1(ck) => Some(ck.1.to_vec()), + #[cfg(feature = "secp256k1")] + Self::Bip340Secp256k1(ck) => Some(ck.1.to_vec()), + #[cfg(feature = "secp256r1")] + Self::EcdsaSecp256r1(ck) => Some(ck.1.to_vec()), + #[cfg(feature = "ed25519")] + Self::Ed25519(ck) => Some(ck.1.to_vec()), + #[cfg(feature = "vetkeys")] + Self::VetKD(_ck) => None, + } + } +} + /// The canister's master public key of a threshold signature system /// /// Each canister gets its own canister master key, which is derived from @@ -420,36 +467,14 @@ impl CanisterMasterKey { /// Return the serialized encoding of the canister master public key pub fn serialize(&self) -> Vec { - match &self.inner { - #[cfg(feature = "secp256k1")] - DerivedPublicKeyInner::EcdsaSecp256k1(ck) => ck.0.serialize_sec1(true), - #[cfg(feature = "secp256k1")] - DerivedPublicKeyInner::Bip340Secp256k1(ck) => ck.0.serialize_sec1(true), - #[cfg(feature = "secp256r1")] - DerivedPublicKeyInner::EcdsaSecp256r1(ck) => ck.0.serialize_sec1(true), - #[cfg(feature = "ed25519")] - DerivedPublicKeyInner::Ed25519(ck) => ck.0.serialize_raw().to_vec(), - #[cfg(feature = "vetkeys")] - DerivedPublicKeyInner::VetKD(ck) => ck.serialize(), - } + self.inner.serialize() } /// Return the chain code used for further derivation, if relevant /// /// Returns None if not applicable for this algorithm pub fn chain_code(&self) -> Option> { - match &self.inner { - #[cfg(feature = "secp256k1")] - DerivedPublicKeyInner::EcdsaSecp256k1(ck) => Some(ck.1.to_vec()), - #[cfg(feature = "secp256k1")] - DerivedPublicKeyInner::Bip340Secp256k1(ck) => Some(ck.1.to_vec()), - #[cfg(feature = "secp256r1")] - DerivedPublicKeyInner::EcdsaSecp256r1(ck) => Some(ck.1.to_vec()), - #[cfg(feature = "ed25519")] - DerivedPublicKeyInner::Ed25519(ck) => Some(ck.1.to_vec()), - #[cfg(feature = "vetkeys")] - DerivedPublicKeyInner::VetKD(_ck) => None, - } + self.inner.chain_code() } } @@ -461,36 +486,14 @@ pub struct DerivedPublicKey { impl DerivedPublicKey { /// Return the serialized encoding of the derived public key pub fn serialize(&self) -> Vec { - match &self.inner { - #[cfg(feature = "secp256k1")] - DerivedPublicKeyInner::EcdsaSecp256k1(ck) => ck.0.serialize_sec1(true), - #[cfg(feature = "secp256k1")] - DerivedPublicKeyInner::Bip340Secp256k1(ck) => ck.0.serialize_sec1(true), - #[cfg(feature = "secp256r1")] - DerivedPublicKeyInner::EcdsaSecp256r1(ck) => ck.0.serialize_sec1(true), - #[cfg(feature = "ed25519")] - DerivedPublicKeyInner::Ed25519(ck) => ck.0.serialize_raw().to_vec(), - #[cfg(feature = "vetkeys")] - DerivedPublicKeyInner::VetKD(ck) => ck.serialize(), - } + self.inner.serialize() } /// Return the chain code used for further derivation, if relevant /// /// Returns None if not applicable for this algorithm pub fn chain_code(&self) -> Option> { - match &self.inner { - #[cfg(feature = "secp256k1")] - DerivedPublicKeyInner::EcdsaSecp256k1(ck) => Some(ck.1.to_vec()), - #[cfg(feature = "secp256k1")] - DerivedPublicKeyInner::Bip340Secp256k1(ck) => Some(ck.1.to_vec()), - #[cfg(feature = "secp256r1")] - DerivedPublicKeyInner::EcdsaSecp256r1(ck) => Some(ck.1.to_vec()), - #[cfg(feature = "ed25519")] - DerivedPublicKeyInner::Ed25519(ck) => Some(ck.1.to_vec()), - #[cfg(feature = "vetkeys")] - DerivedPublicKeyInner::VetKD(_ck) => None, - } + self.inner.chain_code() } } @@ -514,6 +517,10 @@ impl DerivedPublicKey { /// * `pocketic_test_key_1`: The key used by PocketIC when a request to the management canister /// uses `test_key_1`. /// * `dfx_test_key`: PocketIC specific test key +/// +/// All of them exist for both curves: secp256k1 needs the `secp256k1` feature, and +/// secp256r1 needs the `secp256r1` feature. Without the feature for the requested +/// curve, this returns [`Error::AlgorithmNotSupported`]. pub fn derive_ecdsa_key(args: &EcdsaPublicKeyArgs) -> Result { let canister_id = args.canister_id.ok_or(Error::CanisterIdMissing)?; diff --git a/packages/ic-pub-key/tests/tests.rs b/packages/ic-pub-key/tests/tests.rs index 9a2d2862bcb1..9b6b0a44f9d7 100644 --- a/packages/ic-pub-key/tests/tests.rs +++ b/packages/ic-pub-key/tests/tests.rs @@ -1,26 +1,13 @@ use ic_pub_key::*; +#[cfg(any(feature = "secp256k1", feature = "secp256r1"))] fn test_ecdsa_derivation( - key_name: &'static str, - canister_id: &'static str, - derivation_path: &[&'static str], - expected_dpk: &'static str, -) { - test_ecdsa_derivation_with_curve( - EcdsaCurve::Secp256k1, - key_name, - canister_id, - derivation_path, - expected_dpk, - ); -} - -fn test_ecdsa_derivation_with_curve( curve: EcdsaCurve, key_name: &'static str, canister_id: &'static str, derivation_path: &[&'static str], expected_dpk: &'static str, + expected_chain_code: &'static str, ) { use std::str::FromStr; @@ -41,8 +28,46 @@ fn test_ecdsa_derivation_with_curve( let dpk = derive_ecdsa_key(&args).unwrap(); assert_eq!(hex::encode(dpk.public_key), expected_dpk); + assert_eq!(hex::encode(dpk.chain_code), expected_chain_code); +} + +#[cfg(feature = "secp256k1")] +fn test_ecdsa_secp256k1_derivation( + key_name: &'static str, + canister_id: &'static str, + derivation_path: &[&'static str], + expected_dpk: &'static str, + expected_chain_code: &'static str, +) { + test_ecdsa_derivation( + EcdsaCurve::Secp256k1, + key_name, + canister_id, + derivation_path, + expected_dpk, + expected_chain_code, + ); +} + +#[cfg(feature = "secp256r1")] +fn test_ecdsa_secp256r1_derivation( + key_name: &'static str, + canister_id: &'static str, + derivation_path: &[&'static str], + expected_dpk: &'static str, + expected_chain_code: &'static str, +) { + test_ecdsa_derivation( + EcdsaCurve::Secp256r1, + key_name, + canister_id, + derivation_path, + expected_dpk, + expected_chain_code, + ); } +#[cfg(any(feature = "secp256k1", feature = "ed25519"))] fn test_schnorr_derivation( algorithm: SchnorrAlgorithm, key_name: &'static str, @@ -71,6 +96,7 @@ fn test_schnorr_derivation( assert_eq!(hex::encode(dpk.public_key), expected_dpk,); } +#[cfg(feature = "ed25519")] fn test_ed25519_derivation( key_name: &'static str, canister_id: &'static str, @@ -86,6 +112,7 @@ fn test_ed25519_derivation( ); } +#[cfg(feature = "secp256k1")] fn test_bip340_derivation( key_name: &'static str, canister_id: &'static str, @@ -284,7 +311,7 @@ fn test_bip340_derivation_pocketic_dfx_test_key() { #[cfg(feature = "secp256k1")] #[test] fn test_ecdsa_secp256k1_derivation_key1() { - test_ecdsa_derivation( + test_ecdsa_secp256k1_derivation( "key_1", "h5jwf-5iaaa-aaaan-qmvoa-cai", &[ @@ -297,20 +324,22 @@ fn test_ecdsa_secp256k1_derivation_key1() { "key_1", ], "03e99cc5403dfefbc1de767ab34b637ab93c11cecffe89d6475e9701cffa2d51b4", + "e4613655c9f0c9daa769ac5215cd4ef28d02f424c32a0b702a9cd89cbfbf28d7", ); } #[cfg(feature = "secp256k1")] #[test] fn test_ecdsa_secp256k1_derivation_test_key1() { - test_ecdsa_derivation( + test_ecdsa_secp256k1_derivation( "test_key_1", "h5jwf-5iaaa-aaaan-qmvoa-cai", &["Hello", "Threshold", "Signatures"], "0315ae8bb8c6e9f78eec2167f5ac773067f37a39da1a1efbc585f9e90658d1c620", + "366057592ee6212c33dd76f5b4de2ac44a20e861078347fc77d252cbc9d70a5c", ); - test_ecdsa_derivation( + test_ecdsa_secp256k1_derivation( "test_key_1", "h5jwf-5iaaa-aaaan-qmvoa-cai", &[ @@ -323,6 +352,7 @@ fn test_ecdsa_secp256k1_derivation_test_key1() { "test_key_1", ], "0262c4eb6534f278ceebcc8f4172d38acf7d76e1c74ee1e10f362d59f73658ae50", + "95040337537953fc480879186261f689c7fa6a99aab1d1c27c1245692cfa8455", ); } @@ -331,8 +361,7 @@ fn test_ecdsa_secp256k1_derivation_test_key1() { #[cfg(feature = "secp256r1")] #[test] fn test_ecdsa_secp256r1_derivation_key1() { - test_ecdsa_derivation_with_curve( - EcdsaCurve::Secp256r1, + test_ecdsa_secp256r1_derivation( "key_1", "h5jwf-5iaaa-aaaan-qmvoa-cai", &[ @@ -345,14 +374,14 @@ fn test_ecdsa_secp256r1_derivation_key1() { "key_1", ], "02cda0a98456eaf736d750a6b3885433df81b9a5e76c2ee74e4552d9de1a3b38a5", + "a06fa0ac7c5d36631ee816b70a423d35c50af364d589c3e71e956a5b5e4457b4", ); } #[cfg(feature = "secp256r1")] #[test] fn test_ecdsa_secp256r1_derivation_test_key1() { - test_ecdsa_derivation_with_curve( - EcdsaCurve::Secp256r1, + test_ecdsa_secp256r1_derivation( "test_key_1", "h5jwf-5iaaa-aaaan-qmvoa-cai", &[ @@ -365,13 +394,14 @@ fn test_ecdsa_secp256r1_derivation_test_key1() { "test_key_1", ], "038bee03c1155d5b11e08b90fbdebb584c84ba617b916498b9c3d7bdc7a3d32eb8", + "5bea193fe95011f0274a55679d441b9f822abfd829a6482a9b259dc3ed9215e5", ); } #[cfg(feature = "secp256k1")] #[test] fn test_ecdsa_secp256k1_derivation_pocketic_key1() { - test_ecdsa_derivation( + test_ecdsa_secp256k1_derivation( "pocketic_key_1", "uzt4z-lp777-77774-qaabq-cai", &[ @@ -384,13 +414,14 @@ fn test_ecdsa_secp256k1_derivation_pocketic_key1() { "key_1", ], "03bca84b5629dc70a37cadb5b3cda0bfc35abc5658f1d9bf8335b10199785a3836", + "69797aab9c7b3d2498669bab8c5c77656ba168159c9173544d60a6e9c3d7cf25", ); } #[cfg(feature = "secp256k1")] #[test] fn test_ecdsa_secp256k1_derivation_pocketic_test_key1() { - test_ecdsa_derivation( + test_ecdsa_secp256k1_derivation( "pocketic_test_key_1", "uzt4z-lp777-77774-qaabq-cai", &[ @@ -403,13 +434,14 @@ fn test_ecdsa_secp256k1_derivation_pocketic_test_key1() { "test_key_1", ], "022ff35c84bd4cf899707789cfe5db76ce4a650563e678c53e8b128cb4bf4c3763", + "603256c83731d211d638aacfbb82b1964eaabf257dbdcfcf8c2bce5d8c41a425", ); } #[cfg(feature = "secp256k1")] #[test] fn test_ecdsa_secp256k1_derivation_pocketic_dfx_test_key() { - test_ecdsa_derivation( + test_ecdsa_secp256k1_derivation( "dfx_test_key", "uzt4z-lp777-77774-qaabq-cai", &[ @@ -422,14 +454,14 @@ fn test_ecdsa_secp256k1_derivation_pocketic_dfx_test_key() { "dfx_test_key", ], "03f005cf69911ae75f622ce0a621ccddba1a30ea1f6c3d67dd56acbbddb88a9374", + "a0898d1325d632c5851045f32a70f9b635ac74d6eb42a0d4029e8bb5570f4b14", ); } #[cfg(feature = "secp256r1")] #[test] fn test_ecdsa_secp256r1_derivation_pocketic_key1() { - test_ecdsa_derivation_with_curve( - EcdsaCurve::Secp256r1, + test_ecdsa_secp256r1_derivation( "pocketic_key_1", "uzt4z-lp777-77774-qaabq-cai", &[ @@ -442,14 +474,14 @@ fn test_ecdsa_secp256r1_derivation_pocketic_key1() { "key_1", ], "02d3536db48ecef1eb8249f04553046902fbe7d979140530e5294452b94f0e38ad", + "5bda5d523723999fcac4a482118b98b23faef6a9a0968c8984393891ca10a11b", ); } #[cfg(feature = "secp256r1")] #[test] fn test_ecdsa_secp256r1_derivation_pocketic_test_key1() { - test_ecdsa_derivation_with_curve( - EcdsaCurve::Secp256r1, + test_ecdsa_secp256r1_derivation( "pocketic_test_key_1", "uzt4z-lp777-77774-qaabq-cai", &[ @@ -462,14 +494,14 @@ fn test_ecdsa_secp256r1_derivation_pocketic_test_key1() { "test_key_1", ], "03080d5850ca18f277d7f39c3cfd37635b6527a73ba63b774d26d4cdf725e1ce39", + "2ceb3a125d9d9a936b6da671f8b185da23d295ee84855adbca671598c728ff75", ); } #[cfg(feature = "secp256r1")] #[test] fn test_ecdsa_secp256r1_derivation_pocketic_dfx_test_key() { - test_ecdsa_derivation_with_curve( - EcdsaCurve::Secp256r1, + test_ecdsa_secp256r1_derivation( "dfx_test_key", "uzt4z-lp777-77774-qaabq-cai", &[ @@ -482,9 +514,41 @@ fn test_ecdsa_secp256r1_derivation_pocketic_dfx_test_key() { "dfx_test_key", ], "036cd2e52401b60ec8b86041f9c624438eb69ab271def9c9a439bd9fb49fa88ac7", + "3599e883d6781d34d0bf342c0ac973d67fd06e72a93a6fbad691da461d4076f2", + ); +} + +#[cfg(any(feature = "secp256k1", feature = "secp256r1"))] +fn test_ecdsa_derivation_rejects_unknown_key(curve: EcdsaCurve) { + use std::str::FromStr; + + let args = EcdsaPublicKeyArgs { + canister_id: Some(ic_pub_key::CanisterId::from_str("h5jwf-5iaaa-aaaan-qmvoa-cai").unwrap()), + derivation_path: vec![], + key_id: EcdsaKeyId { + curve, + name: "key_2".to_string(), + }, + }; + + assert_eq!( + derive_ecdsa_key(&args).unwrap_err(), + Error::UnknownKeyIdentifier ); } +#[cfg(feature = "secp256k1")] +#[test] +fn test_ecdsa_secp256k1_derivation_rejects_unknown_key() { + test_ecdsa_derivation_rejects_unknown_key(EcdsaCurve::Secp256k1); +} + +#[cfg(feature = "secp256r1")] +#[test] +fn test_ecdsa_secp256r1_derivation_rejects_unknown_key() { + test_ecdsa_derivation_rejects_unknown_key(EcdsaCurve::Secp256r1); +} + #[test] fn test_vetkd_derivation_using_test_key_1() { // This test data was generated on mainnet using test_key_1 From ccadbae91615130bba1e0948aa3872cbd2e92cc0 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 23 Oct 2026 13:33:02 +0000 Subject: [PATCH 6/7] Use the workspace's ic-management-canister-types again #11909 moved the workspace to ic-management-canister-types 0.11.0, which has EcdsaCurve::Secp256r1, so ic-pub-key no longer needs the in-repo crate. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 2 +- packages/ic-pub-key/BUILD.bazel | 2 +- packages/ic-pub-key/Cargo.toml | 5 +---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4eb6d0734587..0bf3a97ba969 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12610,7 +12610,7 @@ dependencies = [ "hex", "ic-cdk-management-canister", "ic-ed25519 0.6.0", - "ic-management-canister-types 0.11.0", + "ic-management-canister-types 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", "ic-secp256k1", "ic-secp256r1", "ic-vetkeys", diff --git a/packages/ic-pub-key/BUILD.bazel b/packages/ic-pub-key/BUILD.bazel index 4d2550e505bb..a7d6365dba1f 100644 --- a/packages/ic-pub-key/BUILD.bazel +++ b/packages/ic-pub-key/BUILD.bazel @@ -17,10 +17,10 @@ rust_library( version = "0.3.0", deps = [ "//packages/ic-ed25519", - "//packages/ic-management-canister-types", "//packages/ic-secp256k1", "//packages/ic-secp256r1", "@crate_index//:ic-cdk-management-canister", + "@crate_index//:ic-management-canister-types", "@crate_index//:ic-vetkeys", ], ) diff --git a/packages/ic-pub-key/Cargo.toml b/packages/ic-pub-key/Cargo.toml index c1f18263b46f..3531057f3b61 100644 --- a/packages/ic-pub-key/Cargo.toml +++ b/packages/ic-pub-key/Cargo.toml @@ -24,10 +24,7 @@ ic-secp256r1 = { path = "../ic-secp256r1", version = "0.3.0", optional = true } ic-ed25519 = { path = "../ic-ed25519", version = "0.6.0", optional = true } ic-vetkeys = { workspace = true, optional = true } ic-cdk-management-canister = { workspace = true, optional = true } -# The in-repo crate, since the workspace's version has no `EcdsaCurve::Secp256r1`. -# This crate re-exports its types, so each breaking release of it also needs a -# breaking release of this crate. -ic-management-canister-types = { path = "../ic-management-canister-types", version = "0.11.0" } +ic-management-canister-types = { workspace = true } [dev-dependencies] hex = { workspace = true } From 401c5c2f37b7149b6c45f058e316a9d232a6e8c6 Mon Sep 17 00:00:00 2001 From: Leo Eichhorn Date: Fri, 23 Oct 2026 16:07:29 +0000 Subject: [PATCH 7/7] Remove an unclear comment from the secp256r1 tests Co-Authored-By: Claude Opus 5.5 --- packages/ic-pub-key/tests/tests.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ic-pub-key/tests/tests.rs b/packages/ic-pub-key/tests/tests.rs index 9b6b0a44f9d7..ab78ab7c031c 100644 --- a/packages/ic-pub-key/tests/tests.rs +++ b/packages/ic-pub-key/tests/tests.rs @@ -356,8 +356,6 @@ fn test_ecdsa_secp256k1_derivation_test_key1() { ); } -// The expected secp256r1 keys are what the replica's `derive_threshold_public_key`, -// which answers `ecdsa_public_key`, returns for these master keys. #[cfg(feature = "secp256r1")] #[test] fn test_ecdsa_secp256r1_derivation_key1() {