Skip to content

Commit

Permalink
Remove potential panics from public API of registered key derivation.
Browse files Browse the repository at this point in the history
Co-authored-by: Kris Nuttycombe <kris@nutty.land>
Signed-off-by: Daira-Emma Hopwood <daira@jacaranda.org>
  • Loading branch information
nuttycom authored and daira committed Feb 20, 2025
1 parent 49c65fc commit 2c177cb
Show file tree
Hide file tree
Showing 2 changed files with 124 additions and 47 deletions.
6 changes: 3 additions & 3 deletions src/hardened_only.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ impl<C: Context> HardenedOnlyKey<C> {
}
I.finalize().as_bytes().try_into().expect("64-byte output")
};
Self::derive_from(&I)
Self::from_bytes(&I)
}

/// Derives a child key from a parent key at a given index and empty tag.
Expand All @@ -107,7 +107,7 @@ impl<C: Context> HardenedOnlyKey<C> {
///
/// [ckdh]: https://zips.z.cash/zip-0032#hardened-only-child-key-derivation
pub fn derive_child_with_tag(&self, index: ChildIndex, tag: &[u8]) -> Self {
Self::derive_from(&self.ckdh_internal(index, 0, tag))
Self::from_bytes(&self.ckdh_internal(index, 0, tag))
}

/// Defined in [ZIP 32: Hardened-only child key derivation][ckdh].
Expand All @@ -130,7 +130,7 @@ impl<C: Context> HardenedOnlyKey<C> {
)
}

fn derive_from(I: &[u8; 64]) -> Self {
fn from_bytes(I: &[u8; 64]) -> Self {
let (I_L, I_R) = I.split_at(32);

// I_L is used as the spending key sk.
Expand Down
165 changes: 121 additions & 44 deletions src/registered.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@
//! [ZIP process]: https://zips.z.cash/zip-0000
//! [`zip32::arbitrary`]: `crate::arbitrary`
use core::fmt::Display;

use zcash_spec::PrfExpand;

use crate::{
Expand All @@ -52,6 +54,66 @@ impl Context for Registered {
const CKD_DOMAIN: HardenedOnlyCkdDomain = PrfExpand::REGISTERED_ZIP32_CHILD;
}

/// An error that occurred in cryptovalue derivation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DerivationError {
/// The provided seed data was invalid. A seed must be between 32 and 252 bytes in length,
/// inclusive.
SeedInvalid,
/// The provided context string is invalid; context strings must be non-empty and no greater
/// than 252 bytes in length.
ContextStringInvalid,
/// The provided subpath was empty. Empty subpaths are not permitted by this API, as the
/// full-width cryptovalue at the empty subpath would be outside the allowed subtree
/// rooted at `m_{context} / zip_number'`.
SubpathEmpty,
}

impl Display for DerivationError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
DerivationError::SeedInvalid => {
write!(f, "Seed must be between 32 and 252 bytes, inclusive.")
}
DerivationError::ContextStringInvalid => write!(
f,
"Context string must be between 1 and 252 bytes, inclusive."
),
DerivationError::SubpathEmpty => write!(
f,
"ZIP 32 registered 64-byte cryptovalue subpaths must have at least one element."
),
}
}
}

#[cfg(feature = "std")]
impl std::error::Error for DerivationError {}

/// A ZIP 32 registered key derivation path element, consisting of a child index and an
/// optionally-empty tag value.
pub struct PathElement<'a> {
child_index: ChildIndex,
tag: &'a [u8],
}

impl<'a> PathElement<'a> {
/// Constructs a new [`PathElement`] from its constituent parts.
pub fn new(child_index: ChildIndex, tag: &'a [u8]) -> Self {
Self { child_index, tag }
}

/// Returns the index at which the child key will be derived.
pub fn child_index(&self) -> ChildIndex {
self.child_index
}

/// Returns the tag that will be used in derivation of the child key.
pub fn tag(&self) -> &[u8] {
self.tag
}
}

/// A registered extended secret key.
///
/// Defined in [ZIP 32: Registered key derivation][regkd].
Expand All @@ -65,30 +127,33 @@ impl SecretKey {
/// Derives a key for a registered application protocol at the given path from the
/// given seed. Each path element may consist of an index and (possibly empty) tag.
///
/// - `zip_number` is the number of the ZIP defining the application protocol.
/// The corresponding hardened index (with empty tag) will be prepended to the
/// `subpath` to obtain the full derivation path.
/// - `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.
/// - `context_string`: an identifier for the context in which this key will be used. It must
/// be globally unique, non-empty, and no more than 252 bytes in length.
/// - `seed`: the root seed. Must be between 32 bytes and 252 bytes in length, inclusive.
/// - `zip_number`: the number of the ZIP defining the application protocol. The corresponding
/// hardened index (with empty tag) will be prepended to the `subpath` to obtain the ZIP 32
/// path.
/// - `subpath`: the path to the desired child element.
pub fn from_subpath(
context_string: &[u8],
seed: &[u8],
zip_number: u16,
subpath: &[(ChildIndex, &[u8])],
) -> Self {
subpath: &[PathElement<'_>],
) -> Result<Self, DerivationError> {
if context_string.is_empty() || context_string.len() > 252 {
return Err(DerivationError::ContextStringInvalid);
}
if seed.len() < 32 || seed.len() > 252 {
return Err(DerivationError::SeedInvalid);
}

let mut xsk = Self::master(context_string, seed)
.derive_child(ChildIndex::hardened(u32::from(zip_number)));

for (i, tag) in subpath {
xsk = xsk.derive_child_with_tag(*i, tag);
for elem in subpath {
xsk = xsk.derive_child_with_tag(elem.child_index, elem.tag);
}
xsk
Ok(xsk)
}

/// Constructs a key for a registered application protocol from its constituent parts.
Expand Down Expand Up @@ -159,49 +224,57 @@ impl SecretKey {
}
}

/// Derives a 64-byte cryptovalue (for use as key material for example), for a
/// registered application protocol at the given non-empty subpath from the given
/// seed. Each subpath element may consist of an index and (possibly empty) tag.
///
/// - `zip_number` is the number of the ZIP defining the application protocol.
/// The corresponding hardened index (with empty tag) will be prepended to the
/// `subpath` to obtain the ZIP 32 path.
/// - `context_string` is an identifier for the context in which this key will be
/// used. It must be globally unique.
///
/// # Panics
/// Derives a 64-byte cryptovalue (for use as key material for example), for a registered
/// application protocol at the given non-empty subpath from the given seed. Each subpath element
/// may consist of an index and a (possibly empty) tag.
///
/// Panics if:
/// - the context string is empty or longer than 252 bytes.
/// - the seed is shorter than 32 bytes or longer than 252 bytes.
/// - the subpath is empty.
/// - `context_string`: an identifier for the context in which this key will be used. It must be
/// globally unique, non-empty, and no more than 252 bytes in length.
/// - `seed`: the root seed. Must be between 32 bytes and 252 bytes in length, inclusive.
/// - `zip_number`: the number of the ZIP defining the application protocol. The corresponding
/// hardened index (with empty tag) will be prepended to the `subpath` to obtain the ZIP 32 path.
/// - `subpath`: the path to the desired child element. A non-empty path is required, in order
/// to ensure that the resulting full-width cryptovalue is within the allowed subtree rooted
/// at `m_{context} / zip_number'`.
pub fn cryptovalue_from_subpath(
context_string: &[u8],
seed: &[u8],
zip_number: u16,
subpath: &[(ChildIndex, &[u8])],
) -> [u8; 64] {
subpath: &[PathElement<'_>],
) -> Result<[u8; 64], DerivationError> {
if context_string.is_empty() || context_string.len() > 252 {
return Err(DerivationError::ContextStringInvalid);
}
if seed.len() < 32 || seed.len() > 252 {
return Err(DerivationError::SeedInvalid);
}
// We can't use NonEmpty because it requires allocation.
assert!(!subpath.is_empty());
if subpath.is_empty() {
return Err(DerivationError::SubpathEmpty);
}

let mut xsk = SecretKey::master(context_string, seed)
.derive_child(ChildIndex::hardened(u32::from(zip_number)));

for (i, tag) in subpath.iter().take(subpath.len() - 1) {
xsk = xsk.derive_child_with_tag(*i, tag);
for elem in subpath.iter().take(subpath.len() - 1) {
xsk = xsk.derive_child_with_tag(elem.child_index, elem.tag);
}
let (i, tag) = subpath.last().expect("nonempty");
xsk.derive_child_cryptovalue(*i, tag)
let elem = subpath.last().expect("nonempty");
Ok(xsk.derive_child_cryptovalue(elem.child_index, elem.tag))
}

#[cfg(test)]
mod tests {
use super::{cryptovalue_from_subpath, ChildIndex, SecretKey};
use crate::registered::PathElement;

use super::{cryptovalue_from_subpath, ChildIndex, DerivationError, SecretKey};

#[test]
#[should_panic]
fn test_cryptovalue_from_empty_subpath_panics() {
cryptovalue_from_subpath(&[0], &[0; 32], 32, &[]);
fn test_cryptovalue_from_empty_subpath_errors() {
assert_eq!(
cryptovalue_from_subpath(&[0], &[0; 32], 32, &[]),
Err(DerivationError::SubpathEmpty),
);
}

struct TestVector {
Expand Down Expand Up @@ -325,15 +398,19 @@ mod tests {
let subpath = tv
.subpath
.iter()
.map(|(i, tag)| (ChildIndex::from_index(*i).expect("hardened"), *tag))
.map(|(i, tag)| {
PathElement::new(ChildIndex::from_index(*i).expect("hardened"), tag)
})
.collect::<alloc::vec::Vec<_>>();

let sk = SecretKey::from_subpath(tv.context_string, &tv.seed, tv.zip_number, &subpath);
let sk = SecretKey::from_subpath(tv.context_string, &tv.seed, tv.zip_number, &subpath)
.unwrap();
assert_eq!(sk.data(), &tv.sk);
assert_eq!(sk.chain_code().as_bytes(), &tv.c);

let fw = (!subpath.is_empty()).then(|| {
cryptovalue_from_subpath(tv.context_string, &tv.seed, tv.zip_number, &subpath)
.unwrap()
});
assert_eq!(&fw, &tv.full_width);
if let Some(fw) = fw {
Expand Down

0 comments on commit 2c177cb

Please sign in to comment.