Implement Bip32 for seed-phrase/passphrase signing (#16942)

* Add Keypair helpers for bip32 derivation

* Plumb bip32 for SignerSourceKind::Ask

* Support full-path querystring

* Use as_ref

* Add public wrappers for from_uri cases

* Support master root derivations (and fix too-deep print

* Add ask:// HD documentation

* Update ASK elsewhere in docs
This commit is contained in:
Tyera Eulberg
2021-05-03 19:58:56 -06:00
committed by GitHub
parent 6318705607
commit 694c674aa6
11 changed files with 471 additions and 65 deletions

View File

@ -44,6 +44,12 @@ impl TryFrom<&str> for DerivationPath {
}
}
impl AsRef<[ChildIndex]> for DerivationPath {
fn as_ref(&self) -> &[ChildIndex] {
&self.0.as_ref()
}
}
impl DerivationPath {
fn new<P: Into<Box<[ChildIndex]>>>(path: P) -> Self {
Self(DerivationPathInner::new(path))
@ -54,8 +60,12 @@ impl DerivationPath {
}
fn from_key_str_with_coin<T: Bip44>(path: &str, coin: T) -> Result<Self, DerivationPathError> {
let path = format!("m/{}", path);
let extend = DerivationPathInner::from_str(&path)
let master_path = if path == "m" {
path.to_string()
} else {
format!("m/{}", path)
};
let extend = DerivationPathInner::from_str(&master_path)
.map_err(|err| DerivationPathError::InvalidDerivationPath(err.to_string()))?;
let mut extend = extend.into_iter();
let account = extend.next().map(|index| index.to_u32());
@ -69,7 +79,7 @@ impl DerivationPath {
Ok(Self::new_bip44_with_coin(coin, account, change))
}
fn _from_absolute_path_str(path: &str) -> Result<Self, DerivationPathError> {
fn from_absolute_path_str(path: &str) -> Result<Self, DerivationPathError> {
let inner = DerivationPath::_from_absolute_path_insecure_str(path)?
.into_iter()
.map(|c| ChildIndex::Hardened(c.to_u32()))
@ -123,8 +133,18 @@ impl DerivationPath {
}
}
// Only accepts single query string pair of type `key`
pub fn from_uri(uri: &URIReference<'_>) -> Result<Option<Self>, DerivationPathError> {
pub fn from_uri_key_query(uri: &URIReference<'_>) -> Result<Option<Self>, DerivationPathError> {
Self::from_uri(uri, true)
}
pub fn from_uri_any_query(uri: &URIReference<'_>) -> Result<Option<Self>, DerivationPathError> {
Self::from_uri(uri, false)
}
fn from_uri(
uri: &URIReference<'_>,
key_only: bool,
) -> Result<Option<Self>, DerivationPathError> {
if let Some(query) = uri.query() {
let query_str = query.as_str();
if query_str.is_empty() {
@ -136,16 +156,26 @@ impl DerivationPath {
"invalid query string, extra fields not supported".to_string(),
));
}
let key = query.get("key");
if key.is_none() {
let key = query.get(QueryKey::Key.as_ref());
if let Some(key) = key {
// Use from_key_str instead of TryInto here to make it more explicit that this
// generates a Solana bip44 DerivationPath
return Self::from_key_str(key).map(Some);
}
if key_only {
return Err(DerivationPathError::InvalidDerivationPath(format!(
"invalid query string `{}`, only `key` supported",
query_str,
)));
}
// Use from_key_str instead of TryInto here to make it a little more explicit that this
// generates a Solana bip44 DerivationPath
key.map(Self::from_key_str).transpose()
let full_path = query.get(QueryKey::FullPath.as_ref());
if let Some(full_path) = full_path {
return Self::from_absolute_path_str(full_path).map(Some);
}
Err(DerivationPathError::InvalidDerivationPath(format!(
"invalid query string `{}`, only `key` and `full-path` supported",
query_str,
)))
} else {
Ok(None)
}
@ -170,6 +200,46 @@ impl<'a> IntoIterator for &'a DerivationPath {
}
}
const QUERY_KEY_FULL_PATH: &str = "full-path";
const QUERY_KEY_KEY: &str = "key";
#[derive(Clone, Debug, Error, PartialEq)]
#[error("invalid query key `{0}`")]
struct QueryKeyError(String);
enum QueryKey {
FullPath,
Key,
}
impl FromStr for QueryKey {
type Err = QueryKeyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let lowercase = s.to_ascii_lowercase();
match lowercase.as_str() {
QUERY_KEY_FULL_PATH => Ok(Self::FullPath),
QUERY_KEY_KEY => Ok(Self::Key),
_ => Err(QueryKeyError(s.to_string())),
}
}
}
impl AsRef<str> for QueryKey {
fn as_ref(&self) -> &str {
match self {
Self::FullPath => QUERY_KEY_FULL_PATH,
Self::Key => QUERY_KEY_KEY,
}
}
}
impl std::fmt::Display for QueryKey {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let s: &str = self.as_ref();
write!(f, "{}", s)
}
}
trait Bip44 {
const PURPOSE: u32 = 44;
const COIN: u32;
@ -240,41 +310,41 @@ mod tests {
fn test_from_absolute_path_str() {
let s = "m/44/501";
assert_eq!(
DerivationPath::_from_absolute_path_str(s).unwrap(),
DerivationPath::from_absolute_path_str(s).unwrap(),
DerivationPath::default()
);
let s = "m/44'/501'";
assert_eq!(
DerivationPath::_from_absolute_path_str(s).unwrap(),
DerivationPath::from_absolute_path_str(s).unwrap(),
DerivationPath::default()
);
let s = "m/44'/501'/1/2";
assert_eq!(
DerivationPath::_from_absolute_path_str(s).unwrap(),
DerivationPath::from_absolute_path_str(s).unwrap(),
DerivationPath::new_bip44(Some(1), Some(2))
);
let s = "m/44'/501'/1'/2'";
assert_eq!(
DerivationPath::_from_absolute_path_str(s).unwrap(),
DerivationPath::from_absolute_path_str(s).unwrap(),
DerivationPath::new_bip44(Some(1), Some(2))
);
// Test non-Solana Bip44
let s = "m/44'/999'/1/2";
assert_eq!(
DerivationPath::_from_absolute_path_str(s).unwrap(),
DerivationPath::from_absolute_path_str(s).unwrap(),
DerivationPath::new_bip44_with_coin(TestCoin, Some(1), Some(2))
);
let s = "m/44'/999'/1'/2'";
assert_eq!(
DerivationPath::_from_absolute_path_str(s).unwrap(),
DerivationPath::from_absolute_path_str(s).unwrap(),
DerivationPath::new_bip44_with_coin(TestCoin, Some(1), Some(2))
);
// Test non-bip44 paths
let s = "m/501'/0'/0/0";
assert_eq!(
DerivationPath::_from_absolute_path_str(s).unwrap(),
DerivationPath::from_absolute_path_str(s).unwrap(),
DerivationPath::new(vec![
ChildIndex::Hardened(501),
ChildIndex::Hardened(0),
@ -284,7 +354,7 @@ mod tests {
);
let s = "m/501'/0'/0'/0'";
assert_eq!(
DerivationPath::_from_absolute_path_str(s).unwrap(),
DerivationPath::from_absolute_path_str(s).unwrap(),
DerivationPath::new(vec![
ChildIndex::Hardened(501),
ChildIndex::Hardened(0),
@ -295,7 +365,7 @@ mod tests {
}
#[test]
fn test_new_from_uri() {
fn test_from_uri() {
let derivation_path = DerivationPath::new_bip44(Some(0), Some(0));
// test://path?key=0/0
@ -311,7 +381,7 @@ mod tests {
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
DerivationPath::from_uri(&uri).unwrap(),
DerivationPath::from_uri(&uri, true).unwrap(),
Some(derivation_path.clone())
);
@ -328,7 +398,7 @@ mod tests {
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
DerivationPath::from_uri(&uri).unwrap(),
DerivationPath::from_uri(&uri, true).unwrap(),
Some(derivation_path.clone())
);
@ -345,10 +415,27 @@ mod tests {
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
DerivationPath::from_uri(&uri).unwrap(),
DerivationPath::from_uri(&uri, true).unwrap(),
Some(derivation_path)
);
// test://path?key=m
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("key=m"))
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
DerivationPath::from_uri(&uri, true).unwrap(),
Some(DerivationPath::new_bip44(None, None))
);
// test://path
let mut builder = URIReferenceBuilder::new();
builder
@ -359,7 +446,7 @@ mod tests {
.try_path("")
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(DerivationPath::from_uri(&uri).unwrap(), None);
assert_eq!(DerivationPath::from_uri(&uri, true).unwrap(), None);
// test://path?
let mut builder = URIReferenceBuilder::new();
@ -373,7 +460,7 @@ mod tests {
.try_query(Some(""))
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(DerivationPath::from_uri(&uri).unwrap(), None);
assert_eq!(DerivationPath::from_uri(&uri, true).unwrap(), None);
// test://path?key=0/0/0
let mut builder = URIReferenceBuilder::new();
@ -388,7 +475,7 @@ mod tests {
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri),
DerivationPath::from_uri(&uri, true),
Err(DerivationPathError::InvalidDerivationPath(_))
));
@ -405,7 +492,7 @@ mod tests {
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri),
DerivationPath::from_uri(&uri, true),
Err(DerivationPathError::InvalidDerivationPath(_))
));
@ -422,7 +509,7 @@ mod tests {
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri),
DerivationPath::from_uri(&uri, true),
Err(DerivationPathError::InvalidDerivationPath(_))
));
@ -439,7 +526,7 @@ mod tests {
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri),
DerivationPath::from_uri(&uri, true),
Err(DerivationPathError::InvalidDerivationPath(_))
));
@ -456,7 +543,7 @@ mod tests {
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri),
DerivationPath::from_uri(&uri, true),
Err(DerivationPathError::InvalidDerivationPath(_))
));
@ -473,7 +560,182 @@ mod tests {
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri),
DerivationPath::from_uri(&uri, true),
Err(DerivationPathError::InvalidDerivationPath(_))
));
}
#[test]
fn test_from_uri_full_path() {
let derivation_path = DerivationPath::from_absolute_path_str("m/44'/999'/1'").unwrap();
// test://path?full-path=m/44/999/1
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("full-path=m/44/999/1"))
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
DerivationPath::from_uri(&uri, false).unwrap(),
Some(derivation_path.clone())
);
// test://path?full-path=m/44'/999'/1'
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("full-path=m/44'/999'/1'"))
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
DerivationPath::from_uri(&uri, false).unwrap(),
Some(derivation_path.clone())
);
// test://path?full-path=m/44\'/999\'/1\'
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("full-path=m/44\'/999\'/1\'"))
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
DerivationPath::from_uri(&uri, false).unwrap(),
Some(derivation_path)
);
// test://path?full-path=m
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("full-path=m"))
.unwrap();
let uri = builder.build().unwrap();
assert_eq!(
DerivationPath::from_uri(&uri, false).unwrap(),
Some(DerivationPath(DerivationPathInner::from_str("m").unwrap()))
);
// test://path?full-path=m/44/999/1, only `key` supported
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("full-path=m/44/999/1"))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri, true),
Err(DerivationPathError::InvalidDerivationPath(_))
));
// test://path?key=0/0&full-path=m/44/999/1
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("key=0/0&full-path=m/44/999/1"))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri, false),
Err(DerivationPathError::InvalidDerivationPath(_))
));
// test://path?full-path=m/44/999/1&bad-key=0/0
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("full-path=m/44/999/1&bad-key=0/0"))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri, false),
Err(DerivationPathError::InvalidDerivationPath(_))
));
// test://path?full-path=bad-value
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("full-path=bad-value"))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri, false),
Err(DerivationPathError::InvalidDerivationPath(_))
));
// test://path?full-path=
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("full-path="))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri, false),
Err(DerivationPathError::InvalidDerivationPath(_))
));
// test://path?full-path
let mut builder = URIReferenceBuilder::new();
builder
.try_scheme(Some("test"))
.unwrap()
.try_authority(Some("path"))
.unwrap()
.try_path("")
.unwrap()
.try_query(Some("full-path"))
.unwrap();
let uri = builder.build().unwrap();
assert!(matches!(
DerivationPath::from_uri(&uri, false),
Err(DerivationPathError::InvalidDerivationPath(_))
));
}

View File

@ -1,8 +1,9 @@
//! The `signature` module provides functionality for public, and private keys.
#![cfg(feature = "full")]
use crate::{pubkey::Pubkey, transaction::TransactionError};
use crate::{derivation_path::DerivationPath, pubkey::Pubkey, transaction::TransactionError};
use ed25519_dalek::Signer as DalekSigner;
use ed25519_dalek_bip32::Error as Bip32Error;
use generic_array::{typenum::U64, GenericArray};
use hmac::Hmac;
use itertools::Itertools;
@ -396,10 +397,37 @@ pub fn keypair_from_seed(seed: &[u8]) -> Result<Keypair, Box<dyn error::Error>>
Ok(Keypair(dalek_keypair))
}
pub fn keypair_from_seed_phrase_and_passphrase(
/// Generates a Keypair using Bip32 Hierarchical Derivation if derivation-path is provided;
/// otherwise builds standard Keypair using the seed as SecretKey
pub fn keypair_from_seed_and_derivation_path(
seed: &[u8],
derivation_path: Option<DerivationPath>,
) -> Result<Keypair, Box<dyn error::Error>> {
if let Some(derivation_path) = derivation_path {
bip32_derived_keypair(seed, derivation_path).map_err(|err| err.to_string().into())
} else {
keypair_from_seed(seed)
}
}
/// Generates a Keypair using Bip32 Hierarchical Derivation
fn bip32_derived_keypair(
seed: &[u8],
derivation_path: DerivationPath,
) -> Result<Keypair, Bip32Error> {
let extended = ed25519_dalek_bip32::ExtendedSecretKey::from_seed(seed)
.and_then(|extended| extended.derive(&derivation_path))?;
let extended_public_key = extended.public_key();
Ok(Keypair(ed25519_dalek::Keypair {
secret: extended.secret_key,
public: extended_public_key,
}))
}
pub fn generate_seed_from_seed_phrase_and_passphrase(
seed_phrase: &str,
passphrase: &str,
) -> Result<Keypair, Box<dyn error::Error>> {
) -> Vec<u8> {
const PBKDF2_ROUNDS: u32 = 2048;
const PBKDF2_BYTES: usize = 64;
@ -412,7 +440,17 @@ pub fn keypair_from_seed_phrase_and_passphrase(
PBKDF2_ROUNDS,
&mut seed,
);
keypair_from_seed(&seed[..])
seed
}
pub fn keypair_from_seed_phrase_and_passphrase(
seed_phrase: &str,
passphrase: &str,
) -> Result<Keypair, Box<dyn error::Error>> {
keypair_from_seed(&generate_seed_from_seed_phrase_and_passphrase(
seed_phrase,
passphrase,
))
}
#[cfg(test)]