Skip to content

Commit 161b12c

Browse files
randombitIDX GitHub Automation
andauthored
feat(crypto): CRP-2579 Add support for derivation to ecdsa_secp256r1 crate (#1730)
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. --------- Co-authored-by: IDX GitHub Automation <infra+github-automation@dfinity.org>
1 parent e70f04d commit 161b12c

File tree

5 files changed

+342
-2
lines changed

5 files changed

+342
-2
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rs/crypto/ecdsa_secp256r1/BUILD.bazel

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ package(default_visibility = ["//visibility:public"])
44

55
DEPENDENCIES = [
66
# Keep sorted.
7+
"@crate_index//:hmac",
78
"@crate_index//:lazy_static",
89
"@crate_index//:num-bigint",
910
"@crate_index//:p256",
1011
"@crate_index//:pem",
1112
"@crate_index//:rand",
1213
"@crate_index//:rand_chacha",
14+
"@crate_index//:sha2",
1315
"@crate_index//:simple_asn1",
1416
"@crate_index//:zeroize",
1517
]
@@ -21,6 +23,7 @@ DEV_DEPENDENCIES = [
2123
"//rs/crypto/sha2",
2224
"//rs/crypto/test_utils/reproducible_rng",
2325
"@crate_index//:hex",
26+
"@crate_index//:hex-literal",
2427
"@crate_index//:wycheproof",
2528
]
2629

rs/crypto/ecdsa_secp256r1/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@ documentation.workspace = true
99
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1010

1111
[dependencies]
12+
hmac = "0.12"
1213
lazy_static = { workspace = true }
1314
num-bigint = { workspace = true }
1415
p256 = { workspace = true }
1516
pem = "1.1.0"
1617
rand = { workspace = true }
1718
rand_chacha = { workspace = true }
19+
sha2 = { workspace = true }
1820
simple_asn1 = { workspace = true }
1921
zeroize = { workspace = true }
2022

2123
[dev-dependencies]
2224
hex = { workspace = true }
25+
hex-literal = "0.4"
2326
ic-crypto-sha2 = { path = "../sha2" }
2427
ic-crypto-test-utils-reproducible-rng = { path = "../test_utils/reproducible_rng" }
2528
wycheproof = { version = "0.6", default-features = false, features = ["ecdsa"] }

rs/crypto/ecdsa_secp256r1/src/lib.rs

Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use p256::{
1010
generic_array::{typenum::Unsigned, GenericArray},
1111
Curve,
1212
},
13-
NistP256,
13+
AffinePoint, NistP256, Scalar,
1414
};
1515
use rand::{CryptoRng, RngCore};
1616
use zeroize::ZeroizeOnDrop;
@@ -35,6 +35,138 @@ lazy_static::lazy_static! {
3535
static ref SECP256R1_OID: simple_asn1::OID = simple_asn1::oid!(1, 2, 840, 10045, 3, 1, 7);
3636
}
3737

38+
/// A component of a derivation path
39+
#[derive(Clone, Debug)]
40+
pub struct DerivationIndex(pub Vec<u8>);
41+
42+
/// Derivation Path
43+
///
44+
/// A derivation path is simply a sequence of DerivationIndex
45+
#[derive(Clone, Debug)]
46+
pub struct DerivationPath {
47+
path: Vec<DerivationIndex>,
48+
}
49+
50+
impl DerivationPath {
51+
/// Create a BIP32-style derivation path
52+
///
53+
/// See SLIP-10 <https://github.com/satoshilabs/slips/blob/master/slip-0010.md>
54+
/// for details of derivation paths
55+
pub fn new_bip32(bip32: &[u32]) -> Self {
56+
let mut path = Vec::with_capacity(bip32.len());
57+
for n in bip32 {
58+
path.push(DerivationIndex(n.to_be_bytes().to_vec()));
59+
}
60+
Self::new(path)
61+
}
62+
63+
/// Create a free-form derivation path
64+
pub fn new(path: Vec<DerivationIndex>) -> Self {
65+
Self { path }
66+
}
67+
68+
/// Create a path from a canister ID and a user provided path
69+
pub fn from_canister_id_and_path(canister_id: &[u8], path: &[Vec<u8>]) -> Self {
70+
let mut vpath = Vec::with_capacity(1 + path.len());
71+
vpath.push(DerivationIndex(canister_id.to_vec()));
72+
73+
for n in path {
74+
vpath.push(DerivationIndex(n.to_vec()));
75+
}
76+
Self::new(vpath)
77+
}
78+
79+
/// Return the length of this path
80+
pub fn len(&self) -> usize {
81+
self.path.len()
82+
}
83+
84+
/// Return if this path is empty
85+
pub fn is_empty(&self) -> bool {
86+
self.len() == 0
87+
}
88+
89+
/// Return the components of the derivation path
90+
pub fn path(&self) -> &[DerivationIndex] {
91+
&self.path
92+
}
93+
94+
fn ckd(idx: &[u8], input: &[u8], chain_code: &[u8; 32]) -> ([u8; 32], Scalar) {
95+
use hmac::{Hmac, Mac};
96+
use p256::elliptic_curve::ops::Reduce;
97+
use sha2::Sha512;
98+
99+
let mut hmac = Hmac::<Sha512>::new_from_slice(chain_code)
100+
.expect("HMAC-SHA-512 should accept 256 bit key");
101+
102+
hmac.update(input);
103+
hmac.update(idx);
104+
105+
let hmac_output: [u8; 64] = hmac.finalize().into_bytes().into();
106+
107+
let fb = p256::FieldBytes::from_slice(&hmac_output[..32]);
108+
let next_offset = <p256::Scalar as Reduce<p256::U256>>::reduce_bytes(fb);
109+
let next_chain_key: [u8; 32] = hmac_output[32..].to_vec().try_into().expect("Correct size");
110+
111+
// If iL >= order, try again with the "next" index as described in SLIP-10
112+
if next_offset.to_bytes().to_vec() != hmac_output[..32] {
113+
let mut next_input = [0u8; 33];
114+
next_input[0] = 0x01;
115+
next_input[1..].copy_from_slice(&next_chain_key);
116+
Self::ckd(idx, &next_input, chain_code)
117+
} else {
118+
(next_chain_key, next_offset)
119+
}
120+
}
121+
122+
fn ckd_pub(
123+
idx: &[u8],
124+
pt: AffinePoint,
125+
chain_code: &[u8; 32],
126+
) -> ([u8; 32], Scalar, AffinePoint) {
127+
use p256::elliptic_curve::{group::GroupEncoding, ops::MulByGenerator};
128+
use p256::ProjectivePoint;
129+
130+
let mut ckd_input = pt.to_bytes();
131+
132+
let pt: ProjectivePoint = pt.into();
133+
134+
loop {
135+
let (next_chain_code, next_offset) = Self::ckd(idx, &ckd_input, chain_code);
136+
137+
let next_pt = (pt + ProjectivePoint::mul_by_generator(&next_offset)).to_affine();
138+
139+
// If the new key is not infinity, we're done: return the new key
140+
if !bool::from(next_pt.is_identity()) {
141+
return (next_chain_code, next_offset, next_pt);
142+
}
143+
144+
// Otherwise set up the next input as defined by SLIP-0010
145+
ckd_input[0] = 0x01;
146+
ckd_input[1..].copy_from_slice(&next_chain_code);
147+
}
148+
}
149+
150+
fn derive_offset(
151+
&self,
152+
pt: AffinePoint,
153+
chain_code: &[u8; 32],
154+
) -> (AffinePoint, Scalar, [u8; 32]) {
155+
let mut offset = Scalar::ZERO;
156+
let mut pt = pt;
157+
let mut chain_code = *chain_code;
158+
159+
for idx in self.path() {
160+
let (next_chain_code, next_offset, next_pt) = Self::ckd_pub(&idx.0, pt, &chain_code);
161+
chain_code = next_chain_code;
162+
pt = next_pt;
163+
offset = offset.add(&next_offset);
164+
}
165+
166+
(pt, offset, chain_code)
167+
}
168+
}
169+
38170
const PEM_HEADER_PKCS8: &str = "PRIVATE KEY";
39171
const PEM_HEADER_RFC5915: &str = "EC PRIVATE KEY";
40172

@@ -305,6 +437,45 @@ impl PrivateKey {
305437
let key = self.key.verifying_key();
306438
PublicKey { key: *key }
307439
}
440+
441+
/// Derive a private key from this private key using a derivation path
442+
///
443+
/// As long as each index of the derivation path is a 4-byte input with the highest
444+
/// bit cleared, this derivation scheme matches SLIP-10
445+
///
446+
pub fn derive_subkey(&self, derivation_path: &DerivationPath) -> (Self, [u8; 32]) {
447+
let chain_code = [0u8; 32];
448+
self.derive_subkey_with_chain_code(derivation_path, &chain_code)
449+
}
450+
451+
/// Derive a private key from this private key using a derivation path
452+
/// and chain code
453+
///
454+
/// As long as each index of the derivation path is a 4-byte input with the highest
455+
/// bit cleared, this derivation scheme matches SLIP-10
456+
///
457+
pub fn derive_subkey_with_chain_code(
458+
&self,
459+
derivation_path: &DerivationPath,
460+
chain_code: &[u8; 32],
461+
) -> (Self, [u8; 32]) {
462+
use p256::NonZeroScalar;
463+
464+
let public_key: AffinePoint = *self.key.verifying_key().as_affine();
465+
let (_pt, offset, derived_chain_code) =
466+
derivation_path.derive_offset(public_key, chain_code);
467+
468+
let derived_scalar = self.key.as_nonzero_scalar().as_ref().add(&offset);
469+
470+
let nz_ds =
471+
NonZeroScalar::new(derived_scalar).expect("Derivation always produces non-zero sum");
472+
473+
let derived_key = Self {
474+
key: p256::ecdsa::SigningKey::from(nz_ds),
475+
};
476+
477+
(derived_key, derived_chain_code)
478+
}
308479
}
309480

310481
/// An ECDSA public key
@@ -399,4 +570,32 @@ impl PublicKey {
399570

400571
self.key.verify_prehash(digest, &signature).is_ok()
401572
}
573+
574+
/// Derive a public key from this public key using a derivation path
575+
///
576+
pub fn derive_subkey(&self, derivation_path: &DerivationPath) -> (Self, [u8; 32]) {
577+
let chain_code = [0u8; 32];
578+
self.derive_subkey_with_chain_code(derivation_path, &chain_code)
579+
}
580+
581+
/// Derive a public key from this public key using a derivation path
582+
/// and chain code
583+
///
584+
/// This derivation matches SLIP-10
585+
pub fn derive_subkey_with_chain_code(
586+
&self,
587+
derivation_path: &DerivationPath,
588+
chain_code: &[u8; 32],
589+
) -> (Self, [u8; 32]) {
590+
let public_key: AffinePoint = *self.key.as_affine();
591+
let (pt, _offset, chain_code) = derivation_path.derive_offset(public_key, chain_code);
592+
593+
let derived_key = Self {
594+
key: p256::ecdsa::VerifyingKey::from(
595+
p256::PublicKey::from_affine(pt).expect("Derived point is valid"),
596+
),
597+
};
598+
599+
(derived_key, chain_code)
600+
}
402601
}

0 commit comments

Comments
 (0)