From 6b3e0118858d094104e56291f8523e5e93acf259 Mon Sep 17 00:00:00 2001 From: Jack Lloyd Date: Fri, 27 Sep 2024 16:43:44 -0400 Subject: [PATCH 1/2] feat(crypto): CRP-2579 Add support for derivation to ecdsa_secp256r1 crate In order to remove the dependency on the internal threshold ECDSA protocol implementation from ic-crypto-utils-canister-threshold-sig we have to have derivation available in the 3 signature utility crates. --- rs/crypto/ecdsa_secp256r1/BUILD.bazel | 3 + rs/crypto/ecdsa_secp256r1/Cargo.toml | 3 + rs/crypto/ecdsa_secp256r1/src/lib.rs | 219 ++++++++++++++++++++++- rs/crypto/ecdsa_secp256r1/tests/tests.rs | 94 +++++++++- 4 files changed, 316 insertions(+), 3 deletions(-) diff --git a/rs/crypto/ecdsa_secp256r1/BUILD.bazel b/rs/crypto/ecdsa_secp256r1/BUILD.bazel index 2dd54e9b91a..d8abaad623b 100644 --- a/rs/crypto/ecdsa_secp256r1/BUILD.bazel +++ b/rs/crypto/ecdsa_secp256r1/BUILD.bazel @@ -4,12 +4,14 @@ package(default_visibility = ["//visibility:public"]) DEPENDENCIES = [ # Keep sorted. + "@crate_index//:hmac", "@crate_index//:lazy_static", "@crate_index//:num-bigint", "@crate_index//:p256", "@crate_index//:pem", "@crate_index//:rand", "@crate_index//:rand_chacha", + "@crate_index//:sha2", "@crate_index//:simple_asn1", "@crate_index//:zeroize", ] @@ -21,6 +23,7 @@ DEV_DEPENDENCIES = [ "//rs/crypto/sha2", "//rs/crypto/test_utils/reproducible_rng", "@crate_index//:hex", + "@crate_index//:hex-literal", "@crate_index//:wycheproof", ] diff --git a/rs/crypto/ecdsa_secp256r1/Cargo.toml b/rs/crypto/ecdsa_secp256r1/Cargo.toml index b7d69b03ebe..c220363b150 100644 --- a/rs/crypto/ecdsa_secp256r1/Cargo.toml +++ b/rs/crypto/ecdsa_secp256r1/Cargo.toml @@ -9,17 +9,20 @@ documentation.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +hmac = "0.12" lazy_static = { workspace = true } num-bigint = { workspace = true } p256 = { workspace = true } pem = "1.1.0" rand = { workspace = true } rand_chacha = { workspace = true } +sha2 = { workspace = true } simple_asn1 = { workspace = true } zeroize = { workspace = true } [dev-dependencies] hex = { workspace = true } +hex-literal = "0.4" ic-crypto-sha2 = { path = "../sha2" } ic-crypto-test-utils-reproducible-rng = { path = "../test_utils/reproducible_rng" } wycheproof = { version = "0.6", default-features = false, features = ["ecdsa"] } diff --git a/rs/crypto/ecdsa_secp256r1/src/lib.rs b/rs/crypto/ecdsa_secp256r1/src/lib.rs index ef97a572556..0828e9dff25 100644 --- a/rs/crypto/ecdsa_secp256r1/src/lib.rs +++ b/rs/crypto/ecdsa_secp256r1/src/lib.rs @@ -10,7 +10,7 @@ use p256::{ generic_array::{typenum::Unsigned, GenericArray}, Curve, }, - NistP256, + AffinePoint, NistP256, Scalar, }; use rand::{CryptoRng, RngCore}; use zeroize::ZeroizeOnDrop; @@ -35,6 +35,135 @@ lazy_static::lazy_static! { static ref SECP256R1_OID: simple_asn1::OID = simple_asn1::oid!(1, 2, 840, 10045, 3, 1, 7); } +/// A component of a derivation path +#[derive(Clone, Debug)] +pub struct DerivationIndex(pub Vec); + +/// Derivation Path +/// +/// A derivation path is simply a sequence of DerivationIndex +#[derive(Clone, Debug)] +pub struct DerivationPath { + path: Vec, +} + +impl DerivationPath { + /// Create a BIP32-style derivation path + pub fn new_bip32(bip32: &[u32]) -> Self { + let mut path = Vec::with_capacity(bip32.len()); + for n in bip32 { + path.push(DerivationIndex(n.to_be_bytes().to_vec())); + } + Self::new(path) + } + + /// Create a free-form derivation path + pub fn new(path: Vec) -> Self { + Self { path } + } + + /// Create a path from a canister ID and a user provided path + pub fn from_canister_id_and_path(canister_id: &[u8], path: &[Vec]) -> Self { + let mut vpath = Vec::with_capacity(1 + path.len()); + vpath.push(DerivationIndex(canister_id.to_vec())); + + for n in path { + vpath.push(DerivationIndex(n.to_vec())); + } + Self::new(vpath) + } + + /// Return the length of this path + pub fn len(&self) -> usize { + self.path.len() + } + + /// Return if this path is empty + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Return the components of the derivation path + pub fn path(&self) -> &[DerivationIndex] { + &self.path + } + + fn ckd(idx: &[u8], input: &[u8], chain_code: &[u8; 32]) -> ([u8; 32], Scalar) { + use hmac::{Hmac, Mac}; + use p256::elliptic_curve::ops::Reduce; + use sha2::Sha512; + + let mut hmac = Hmac::::new_from_slice(chain_code) + .expect("HMAC-SHA-512 should accept 256 bit key"); + + hmac.update(input); + hmac.update(idx); + + let hmac_output: [u8; 64] = hmac.finalize().into_bytes().into(); + + let fb = p256::FieldBytes::from_slice(&hmac_output[..32]); + let next_offset = >::reduce_bytes(fb); + let next_chain_key: [u8; 32] = hmac_output[32..].to_vec().try_into().expect("Correct size"); + + // If iL >= order, try again with the "next" index as described in SLIP-10 + if next_offset.to_bytes().to_vec() != hmac_output[..32] { + let mut next_input = [0u8; 33]; + next_input[0] = 0x01; + next_input[1..].copy_from_slice(&next_chain_key); + Self::ckd(idx, &next_input, chain_code) + } else { + (next_chain_key, next_offset) + } + } + + fn ckd_pub( + idx: &[u8], + pt: AffinePoint, + chain_code: &[u8; 32], + ) -> ([u8; 32], Scalar, AffinePoint) { + use p256::elliptic_curve::{group::GroupEncoding, ops::MulByGenerator}; + use p256::ProjectivePoint; + + let mut ckd_input = pt.to_bytes(); + + let pt: ProjectivePoint = pt.into(); + + loop { + let (next_chain_code, next_offset) = Self::ckd(idx, &ckd_input, chain_code); + + let next_pt = (pt + ProjectivePoint::mul_by_generator(&next_offset)).to_affine(); + + // If the new key is not infinity, we're done: return the new key + if !bool::from(next_pt.is_identity()) { + return (next_chain_code, next_offset, next_pt); + } + + // Otherwise set up the next input as defined by SLIP-0010 + ckd_input[0] = 0x01; + ckd_input[1..].copy_from_slice(&next_chain_code); + } + } + + fn derive_offset( + &self, + pt: AffinePoint, + chain_code: &[u8; 32], + ) -> (AffinePoint, Scalar, [u8; 32]) { + let mut offset = Scalar::ZERO; + let mut pt = pt; + let mut chain_code = *chain_code; + + for idx in self.path() { + let (next_chain_code, next_offset, next_pt) = Self::ckd_pub(&idx.0, pt, &chain_code); + chain_code = next_chain_code; + pt = next_pt; + offset = offset.add(&next_offset); + } + + (pt, offset, chain_code) + } +} + const PEM_HEADER_PKCS8: &str = "PRIVATE KEY"; const PEM_HEADER_RFC5915: &str = "EC PRIVATE KEY"; @@ -305,6 +434,57 @@ impl PrivateKey { let key = self.key.verifying_key(); PublicKey { key: *key } } + + /// Derive a private key from this private key using a derivation path + /// + /// This is the same derivation system used by the Internet Computer when + /// deriving subkeys for threshold ECDSA with secp256r1 + /// + /// As long as each index of the derivation path is a 4-byte input with the highest + /// bit cleared, this derivation scheme matches SLIP-10 + /// + /// See + /// for details on the derivation scheme. + /// + pub fn derive_subkey(&self, derivation_path: &DerivationPath) -> (Self, [u8; 32]) { + let chain_code = [0u8; 32]; + self.derive_subkey_with_chain_code(derivation_path, &chain_code) + } + + /// Derive a private key from this private key using a derivation path + /// and chain code + /// + /// This is the same derivation system used by the Internet Computer when + /// deriving subkeys for threshold ECDSA with secp256r1 + /// + /// As long as each index of the derivation path is a 4-byte input with the highest + /// bit cleared, this derivation scheme matches SLIP-10 + /// + /// See + /// for details on the derivation scheme. + /// + pub fn derive_subkey_with_chain_code( + &self, + derivation_path: &DerivationPath, + chain_code: &[u8; 32], + ) -> (Self, [u8; 32]) { + use p256::NonZeroScalar; + + let public_key: AffinePoint = self.key.verifying_key().as_affine().clone(); + let (_pt, offset, derived_chain_code) = + derivation_path.derive_offset(public_key, chain_code); + + let derived_scalar = self.key.as_nonzero_scalar().as_ref().add(&offset); + + let nz_ds = + NonZeroScalar::new(derived_scalar).expect("Derivation always produces non-zero sum"); + + let derived_key = Self { + key: p256::SecretKey::from(nz_ds).into(), + }; + + (derived_key, derived_chain_code) + } } /// An ECDSA public key @@ -374,7 +554,7 @@ impl PublicKey { /// valid signature, then (r,-s) is also valid. To avoid this malleability, /// some systems require that s be "normalized" to the smallest value. /// - /// This normalization is quite common on secp256k1, but is virtually + /// This normalization is quite common on secp256r1, but is virtually /// unknown and unimplemented for secp256r1. The vast majority of secp256r1 /// signatures will not be normalized. Thus this verification *does not* /// ensure any non-malleability properties. @@ -399,4 +579,39 @@ impl PublicKey { self.key.verify_prehash(digest, &signature).is_ok() } + + /// Derive a public key from this public key using a derivation path + /// + /// This is the same derivation system used by the Internet Computer when + /// deriving subkeys for threshold ECDSA with secp256r1 + /// + pub fn derive_subkey(&self, derivation_path: &DerivationPath) -> (Self, [u8; 32]) { + let chain_code = [0u8; 32]; + self.derive_subkey_with_chain_code(derivation_path, &chain_code) + } + + /// Derive a public key from this public key using a derivation path + /// and chain code + /// + /// This is the same derivation system used by the Internet Computer when + /// deriving subkeys for threshold ECDSA with secp256r1 + /// + /// This derivation matches SLIP-10 + pub fn derive_subkey_with_chain_code( + &self, + derivation_path: &DerivationPath, + chain_code: &[u8; 32], + ) -> (Self, [u8; 32]) { + let public_key: AffinePoint = *self.key.as_affine(); + let (pt, _offset, chain_code) = derivation_path.derive_offset(public_key, chain_code); + + let derived_key = Self { + key: p256::PublicKey::from( + p256::PublicKey::from_affine(pt).expect("Derived point is valid"), + ) + .into(), + }; + + (derived_key, chain_code) + } } diff --git a/rs/crypto/ecdsa_secp256r1/tests/tests.rs b/rs/crypto/ecdsa_secp256r1/tests/tests.rs index 9a93463dc7b..94e7133fb32 100644 --- a/rs/crypto/ecdsa_secp256r1/tests/tests.rs +++ b/rs/crypto/ecdsa_secp256r1/tests/tests.rs @@ -1,4 +1,5 @@ -use ic_crypto_ecdsa_secp256r1::{KeyDecodingError, PrivateKey, PublicKey}; +use hex_literal::hex; +use ic_crypto_ecdsa_secp256r1::*; use ic_crypto_test_utils_reproducible_rng::reproducible_rng; #[test] @@ -271,3 +272,94 @@ NRLvCGaIxJfchxpjcCysTG12MfKOf6/Phw== SAMPLE_SECP256R1_5915_PEM ); } + +#[test] +fn private_derivation_is_compatible_with_public_derivation() { + use rand::Rng; + + let rng = &mut reproducible_rng(); + + fn random_path(rng: &mut R) -> DerivationPath { + let l = 1 + rng.gen::() % 9; + let path = (0..l).map(|_| rng.gen::()).collect::>(); + DerivationPath::new_bip32(&path) + } + + for _ in 0..100 { + let master_sk = PrivateKey::generate_using_rng(rng); + let master_pk = master_sk.public_key(); + + let path = random_path(rng); + + let chain_code = rng.gen::<[u8; 32]>(); + + let (derived_pk, cc_pk) = master_pk.derive_subkey_with_chain_code(&path, &chain_code); + + let (derived_sk, cc_sk) = master_sk.derive_subkey_with_chain_code(&path, &chain_code); + + assert_eq!( + hex::encode(derived_pk.serialize_sec1(true)), + hex::encode(derived_sk.public_key().serialize_sec1(true)) + ); + + assert_eq!(hex::encode(cc_pk), hex::encode(cc_sk)); + + let msg = rng.gen::<[u8; 32]>(); + let derived_sig = derived_sk.sign_message(&msg); + + assert!(derived_pk.verify_signature(&msg, &derived_sig)); + } +} + +#[test] +fn should_match_slip10_derivation_test_data() { + // Test data from https://github.com/satoshilabs/slips/blob/master/slip-0010.md#test-vector-1-for-nist256p1 + let chain_code = hex!("98c7514f562e64e74170cc3cf304ee1ce54d6b6da4f880f313e8204c2a185318"); + + let private_key = PrivateKey::deserialize_sec1(&hex!( + "694596e8a54f252c960eb771a3c41e7e32496d03b954aeb90f61635b8e092aa7" + )) + .expect("Test has valid key"); + + let public_key = PublicKey::deserialize_sec1(&hex!( + "0359cf160040778a4b14c5f4d7b76e327ccc8c4a6086dd9451b7482b5a4972dda0" + )) + .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(&[2, 1000000000]); + + 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), + "b9b7b82d326bb9cb5b5b121066feea4eb93d5241103c9e7a18aad40f1dde8059", + ); + assert_eq!( + hex::encode(pk_chain_code), + "b9b7b82d326bb9cb5b5b121066feea4eb93d5241103c9e7a18aad40f1dde8059", + ); + + assert_eq!( + hex::encode(derived_public_key.serialize_sec1(true)), + "02216cd26d31147f72427a453c443ed2cde8a1e53c9cc44e5ddf739725413fe3f4", + ); + + assert_eq!( + hex::encode(derived_secret_key.serialize_sec1()), + "21c4f269ef0a5fd1badf47eeacebeeaa3de22eb8e5b0adcd0f27dd99d34d0119", + ); + + assert_eq!( + hex::encode(derived_public_key.serialize_sec1(true)), + hex::encode(derived_secret_key.public_key().serialize_sec1(true)), + "Derived keys match" + ); +} From 43cde17358460805aaf6a7ee0b6508513ec7b82c Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Fri, 27 Sep 2024 20:46:17 +0000 Subject: [PATCH 2/2] Automatically updated Cargo*.lock --- Cargo.lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index d28bdcebfdc..92e885dc759 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6821,6 +6821,8 @@ name = "ic-crypto-ecdsa-secp256r1" version = "0.9.0" dependencies = [ "hex", + "hex-literal", + "hmac", "ic-crypto-sha2", "ic-crypto-test-utils-reproducible-rng", "lazy_static", @@ -6829,6 +6831,7 @@ dependencies = [ "pem 1.1.1", "rand 0.8.5", "rand_chacha 0.3.1", + "sha2 0.10.8", "simple_asn1", "wycheproof", "zeroize",