-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from zcash/arb-keys
Implement ZIP 32 arbitrary key derivation
- Loading branch information
Showing
6 changed files
with
261 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
[package] | ||
name = "zip32" | ||
version = "0.1.1" | ||
version = "0.1.2" | ||
authors = [ | ||
"Jack Grigg <[email protected]>", | ||
"Kris Nuttycombe <[email protected]>", | ||
|
@@ -17,6 +17,7 @@ rust-version = "1.60" | |
blake2b_simd = "1" | ||
memuse = "0.2.1" | ||
subtle = "2.2.3" | ||
zcash_spec = "0.1.2" | ||
|
||
[dev-dependencies] | ||
assert_matches = "1.5" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
//! Arbitrary key derivation. | ||
//! | ||
//! In some contexts there is a need for deriving arbitrary keys with the same derivation | ||
//! path as existing key material (for example, deriving an arbitrary account-level key), | ||
//! without the need for ecosystem-wide coordination. The following instantiation of the | ||
//! [hardened key generation framework] may be used for this purpose. | ||
//! | ||
//! Defined in [ZIP32: Arbitrary key derivation][arbkd]. | ||
//! | ||
//! [hardened key generation framework]: crate::hardened_only | ||
//! [arbkd]: https://zips.z.cash/zip-0032#specification-arbitrary-key-derivation | ||
|
||
use zcash_spec::PrfExpand; | ||
|
||
use crate::{ | ||
hardened_only::{Context, HardenedOnlyKey}, | ||
ChainCode, ChildIndex, | ||
}; | ||
|
||
struct Arbitrary; | ||
|
||
impl Context for Arbitrary { | ||
const MKG_DOMAIN: [u8; 16] = *b"ZcashArbitraryKD"; | ||
const CKD_DOMAIN: PrfExpand<([u8; 32], [u8; 4])> = PrfExpand::ARBITRARY_ZIP32_CHILD; | ||
} | ||
|
||
/// An arbitrary extended secret key. | ||
/// | ||
/// Defined in [ZIP32: Arbitrary key derivation][arbkd]. | ||
/// | ||
/// [arbkd]: https://zips.z.cash/zip-0032#specification-arbitrary-key-derivation | ||
pub struct SecretKey { | ||
inner: HardenedOnlyKey<Arbitrary>, | ||
} | ||
|
||
impl SecretKey { | ||
/// Derives an arbitrary key at the given path from the given seed. | ||
/// | ||
/// `context_string` is an identifier for the context in which this key will be used. | ||
/// It must be globally unique. | ||
/// | ||
/// # Panics | ||
/// | ||
/// Panics if: | ||
/// - the context string is empty or longer than 252 bytes. | ||
/// - the seed is shorter than 32 bytes or longer than 252 bytes. | ||
pub fn from_path(context_string: &[u8], seed: &[u8], path: &[ChildIndex]) -> Self { | ||
let mut xsk = Self::master(context_string, seed); | ||
for i in path { | ||
xsk = xsk.derive_child(*i); | ||
} | ||
xsk | ||
} | ||
|
||
/// Generates the master key of an Arbitrary extended secret key. | ||
/// | ||
/// Defined in [ZIP32: Arbitrary master key generation][mkgarb]. | ||
/// | ||
/// [mkgarb]: https://zips.z.cash/zip-0032#arbitrary-master-key-generation | ||
/// | ||
/// # Panics | ||
/// | ||
/// Panics if: | ||
/// - the context string is empty or longer than 252 bytes. | ||
/// - the seed is shorter than 32 bytes or longer than 252 bytes. | ||
fn master(context_string: &[u8], seed: &[u8]) -> Self { | ||
let context_len = | ||
u8::try_from(context_string.len()).expect("context string should be at most 252 bytes"); | ||
assert!((1..=252).contains(&context_len)); | ||
|
||
let seed_len = u8::try_from(seed.len()).expect("seed should be at most 252 bytes"); | ||
assert!((32..=252).contains(&seed_len)); | ||
|
||
let ikm = &[&[context_len], context_string, &[seed_len], seed]; | ||
|
||
Self { | ||
inner: HardenedOnlyKey::master(ikm), | ||
} | ||
} | ||
|
||
/// Derives a child key from a parent key at a given index. | ||
/// | ||
/// Defined in [ZIP32: Arbitrary-only child key derivation][ckdarb]. | ||
/// | ||
/// [ckdarb]: https://zips.z.cash/zip-0032#arbitrary-child-key-derivation | ||
fn derive_child(&self, index: ChildIndex) -> Self { | ||
Self { | ||
inner: self.inner.derive_child(index), | ||
} | ||
} | ||
|
||
/// Returns the key material for this arbitrary key. | ||
pub fn data(&self) -> &[u8; 32] { | ||
self.inner.parts().0 | ||
} | ||
|
||
/// Returns the chain code for this arbitrary key. | ||
pub fn chain_code(&self) -> &ChainCode { | ||
self.inner.parts().1 | ||
} | ||
|
||
/// Concatenates the key data and chain code to obtain a full-width key. | ||
/// | ||
/// This may be used when a context requires a 64-byte key instead of a 32-byte key | ||
/// (for example, to avoid an entropy bottleneck in its particular subsequent | ||
/// operations). | ||
/// | ||
/// Child keys MUST NOT be derived from any key on which this method is called. For | ||
/// the current API, this means that [`SecretKey::from_path`] MUST NOT be called with | ||
/// a `path` for which this key's path is a prefix. | ||
pub fn into_full_width_key(self) -> [u8; 64] { | ||
let (sk, c) = self.inner.into_parts(); | ||
// Re-concatenate the key parts. | ||
let mut key = [0; 64]; | ||
key[..32].copy_from_slice(&sk); | ||
key[32..].copy_from_slice(&c.0); | ||
key | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
//! Generic framework for hardened-only key derivation. | ||
//! | ||
//! Defined in [ZIP32: Hardened-only key derivation][hkd]. | ||
//! | ||
//! Any usage of the types in this module needs to have a corresponding ZIP. If you just | ||
//! want to derive an arbitrary key in a ZIP 32-compatible manner without ecosystem-wide | ||
//! coordination, use [`arbitrary::SecretKey`]. | ||
//! | ||
//! [hkd]: https://zips.z.cash/zip-0032#specification-hardened-only-key-derivation | ||
//! [`arbitrary::SecretKey`]: crate::arbitrary::SecretKey | ||
|
||
use core::marker::PhantomData; | ||
|
||
use blake2b_simd::Params as Blake2bParams; | ||
use subtle::{Choice, ConstantTimeEq}; | ||
use zcash_spec::PrfExpand; | ||
|
||
use crate::{ChainCode, ChildIndex}; | ||
|
||
/// The context in which hardened-only key derivation is instantiated. | ||
pub trait Context { | ||
/// A 16-byte domain separator used during master key generation. | ||
/// | ||
/// It SHOULD be disjoint from other domain separators used with BLAKE2b in Zcash | ||
/// protocols. | ||
const MKG_DOMAIN: [u8; 16]; | ||
/// The `PrfExpand` domain used during child key derivation. | ||
const CKD_DOMAIN: PrfExpand<([u8; 32], [u8; 4])>; | ||
} | ||
|
||
/// An arbitrary extended secret key. | ||
/// | ||
/// Defined in [ZIP32: Hardened-only key derivation][hkd]. | ||
/// | ||
/// [hkd]: https://zips.z.cash/zip-0032#specification-hardened-only-key-derivation | ||
#[derive(Clone, Debug)] | ||
pub struct HardenedOnlyKey<C: Context> { | ||
sk: [u8; 32], | ||
chain_code: ChainCode, | ||
_context: PhantomData<C>, | ||
} | ||
|
||
impl<C: Context> ConstantTimeEq for HardenedOnlyKey<C> { | ||
fn ct_eq(&self, rhs: &Self) -> Choice { | ||
self.chain_code.ct_eq(&rhs.chain_code) & self.sk.ct_eq(&rhs.sk) | ||
} | ||
} | ||
|
||
#[allow(non_snake_case)] | ||
impl<C: Context> HardenedOnlyKey<C> { | ||
/// Exposes the parts of this key. | ||
pub fn parts(&self) -> (&[u8; 32], &ChainCode) { | ||
(&self.sk, &self.chain_code) | ||
} | ||
|
||
/// Decomposes this key into its parts. | ||
pub(crate) fn into_parts(self) -> ([u8; 32], ChainCode) { | ||
(self.sk, self.chain_code) | ||
} | ||
|
||
/// Generates the master key of a hardened-only extended secret key. | ||
/// | ||
/// Defined in [ZIP32: Hardened-only master key generation][mkgh]. | ||
/// | ||
/// [mkgh]: https://zips.z.cash/zip-0032#hardened-only-master-key-generation | ||
pub fn master(ikm: &[&[u8]]) -> Self { | ||
// I := BLAKE2b-512(Context.MKGDomain, IKM) | ||
let I: [u8; 64] = { | ||
let mut I = Blake2bParams::new() | ||
.hash_length(64) | ||
.personal(&C::MKG_DOMAIN) | ||
.to_state(); | ||
for input in ikm { | ||
I.update(input); | ||
} | ||
I.finalize().as_bytes().try_into().unwrap() | ||
}; | ||
|
||
let (I_L, I_R) = I.split_at(32); | ||
|
||
// I_L is used as the master secret key sk_m. | ||
let sk_m = I_L.try_into().unwrap(); | ||
|
||
// I_R is used as the master chain code c_m. | ||
let c_m = ChainCode::new(I_R.try_into().unwrap()); | ||
|
||
Self { | ||
sk: sk_m, | ||
chain_code: c_m, | ||
_context: PhantomData, | ||
} | ||
} | ||
|
||
/// Derives a child key from a parent key at a given index. | ||
/// | ||
/// Defined in [ZIP32: Hardened-only child key derivation][ckdh]. | ||
/// | ||
/// [ckdh]: https://zips.z.cash/zip-0032#hardened-only-child-key-derivation | ||
pub fn derive_child(&self, index: ChildIndex) -> Self { | ||
// I := PRF^Expand(c_par, [Context.CKDDomain] || sk_par || I2LEOSP(i)) | ||
let I: [u8; 64] = C::CKD_DOMAIN.with( | ||
self.chain_code.as_bytes(), | ||
&self.sk, | ||
&index.index().to_le_bytes(), | ||
); | ||
|
||
let (I_L, I_R) = I.split_at(32); | ||
|
||
// I_L is used as the child spending key sk_i. | ||
let sk_i = I_L.try_into().unwrap(); | ||
|
||
// I_R is used as the child chain code c_i. | ||
let c_i = ChainCode::new(I_R.try_into().unwrap()); | ||
|
||
Self { | ||
sk: sk_i, | ||
chain_code: c_i, | ||
_context: PhantomData, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters