Skip to content

Commit

Permalink
crypto: Add BIP32 derivation to keygen
Browse files Browse the repository at this point in the history
  • Loading branch information
joyqvq committed Sep 15, 2022
1 parent d242c08 commit 71fd0a1
Show file tree
Hide file tree
Showing 19 changed files with 663 additions and 296 deletions.
284 changes: 103 additions & 181 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions crates/sui-sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ signature = "1.6.0"
tokio = "1.20.1"
rand = "0.8.5"
bcs = "0.1.3"

bip39 = { git = "https://github.com/patrickkuo/rust-bip39.git" , rev = "a76fe8310416555e6383b42b8acc4eb93c7bcc89", features = ["rand"]}
tiny-bip39 = "1.0.0"
bip32 = "0.4.0"

sui-json-rpc = { path = "../sui-json-rpc" }
sui-json-rpc-types= { path = "../sui-json-rpc-types" }
Expand Down
61 changes: 19 additions & 42 deletions crates/sui-sdk/src/crypto.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
// Copyright (c) 2022, Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

// TODO: Remove usage of rand::rngs::adapter::ReadRng.
#![allow(deprecated)]

use anyhow::anyhow;
use bip32::DerivationPath;
use bip39::{Language, Mnemonic, MnemonicType, Seed};
use rand::{rngs::StdRng, SeedableRng};
use serde::{Deserialize, Serialize};
use signature::Signer;
Expand All @@ -15,15 +14,11 @@ use std::fs;
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use bip39::Mnemonic;
use rand::rngs::adapter::ReadRng;

use sui_types::base_types::SuiAddress;
use sui_types::crypto::{
get_key_pair_from_rng, random_key_pair_by_type_from_rng, EncodeDecodeBase64, PublicKey,
Signature, SignatureScheme, SuiKeyPair,
derive_key_pair_from_path, get_key_pair_from_rng, EncodeDecodeBase64, PublicKey, Signature,
SignatureScheme, SuiKeyPair,
};

#[derive(Serialize, Deserialize)]
Expand Down Expand Up @@ -157,15 +152,17 @@ impl SuiKeystore {
pub fn generate_new_key(
&mut self,
key_scheme: SignatureScheme,
derivation_path: Option<DerivationPath>,
) -> Result<(SuiAddress, String, SignatureScheme), anyhow::Error> {
let mnemonic = Mnemonic::generate(12)?;
let seed = mnemonic.to_seed("");
let mut rng = RngWrapper(ReadRng::new(&seed));
match random_key_pair_by_type_from_rng(key_scheme, &mut rng) {
Ok((address, kp)) => {
let k = kp.public();
self.0.add_key(kp)?;
Ok((address, mnemonic.to_string(), k.scheme()))
let mnemonic = Mnemonic::new(MnemonicType::Words12, Language::English);
match derive_key_pair_from_path(
Seed::new(&mnemonic, "").as_bytes(),
derivation_path,
&key_scheme,
) {
Ok((address, keypair)) => {
self.add_key(keypair)?;
Ok((address, mnemonic.phrase().to_string(), key_scheme))
}
Err(e) => Err(anyhow!("error generating key {:?}", e)),
}
Expand All @@ -187,10 +184,12 @@ impl SuiKeystore {
&mut self,
phrase: &str,
key_scheme: SignatureScheme,
derivation_path: Option<DerivationPath>,
) -> Result<SuiAddress, anyhow::Error> {
let seed = &Mnemonic::from_str(phrase).unwrap().to_seed("");
let mut rng = RngWrapper(ReadRng::new(seed));
match random_key_pair_by_type_from_rng(key_scheme, &mut rng) {
let mnemonic = Mnemonic::from_phrase(phrase, Language::English)
.map_err(|e| anyhow::anyhow!("Invalid mnemonic phrase: {:?}", e))?;
let seed = Seed::new(&mnemonic, "");
match derive_key_pair_from_path(seed.as_bytes(), derivation_path, &key_scheme) {
Ok((address, kp)) => {
self.0.add_key(kp)?;
Ok(address)
Expand All @@ -204,28 +203,6 @@ impl SuiKeystore {
}
}

/// wrapper for adding CryptoRng and RngCore impl to ReadRng.
struct RngWrapper<'a>(ReadRng<&'a [u8]>);

impl rand::CryptoRng for RngWrapper<'_> {}
impl rand::RngCore for RngWrapper<'_> {
fn next_u32(&mut self) -> u32 {
self.0.next_u32()
}

fn next_u64(&mut self) -> u64 {
self.0.next_u64()
}

fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest)
}

fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand::Error> {
self.0.try_fill_bytes(dest)
}
}

struct KeystoreSigner<'a> {
keystore: &'a dyn AccountKeystore,
address: SuiAddress,
Expand Down
15 changes: 8 additions & 7 deletions crates/sui-sdk/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@ fn mnemonic_test() {
let temp_dir = TempDir::new().unwrap();
let keystore_path = temp_dir.path().join("sui.keystore");
let mut keystore = KeystoreType::File(keystore_path).init().unwrap();

let (address, phrase, scheme) = keystore.generate_new_key(SignatureScheme::ED25519).unwrap();
let (address, phrase, scheme) = keystore
.generate_new_key(SignatureScheme::ED25519, None)
.unwrap();

let keystore_path_2 = temp_dir.path().join("sui2.keystore");
let mut keystore2 = KeystoreType::File(keystore_path_2).init().unwrap();
let imported_address = keystore2
.import_from_mnemonic(&phrase, SignatureScheme::ED25519)
.import_from_mnemonic(&phrase, SignatureScheme::ED25519, None)
.unwrap();
assert_eq!(scheme.flag(), Ed25519SuiSignature::SCHEME.flag());
assert_eq!(address, imported_address);
Expand All @@ -31,22 +32,22 @@ fn mnemonic_test() {
/// This test confirms rust's implementation of mnemonic is the same with the Sui Wallet
#[test]
fn sui_wallet_address_mnemonic_test() -> Result<(), anyhow::Error> {
// Recovery phase and SuiAddress obtained from Sui wallet v0.0.4 (prior key flag changes)
let phrase = "oil puzzle immense upon pony govern jelly neck portion laptop laptop wall";
let expected_address = SuiAddress::from_str("0x6a06dd564dfb2f0c71f3e167a48f569c705ed34c")?;
let phrase = "result crisp session latin must fruit genuine question prevent start coconut brave speak student dismiss";
let expected_address = SuiAddress::from_str("0x1a4623343cd42be47d67314fce0ad042f3c82685")?;

let temp_dir = TempDir::new().unwrap();
let keystore_path = temp_dir.path().join("sui.keystore");
let mut keystore = KeystoreType::File(keystore_path).init().unwrap();

keystore
.import_from_mnemonic(phrase, SignatureScheme::ED25519)
.import_from_mnemonic(phrase, SignatureScheme::ED25519, None)
.unwrap();

let pubkey = keystore.keys()[0].clone();
assert_eq!(pubkey.flag(), Ed25519SuiSignature::SCHEME.flag());

let mut hasher = Sha3_256::default();
hasher.update([pubkey.flag()]);
hasher.update(pubkey);
let g_arr = hasher.finalize();
let mut res = [0u8; SUI_ADDRESS_LENGTH];
Expand Down
2 changes: 2 additions & 0 deletions crates/sui-types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ strum_macros = "^0.24"
roaring = "0.10.1"
enum_dispatch = "^0.3"
eyre = "0.6.8"
bip32 = "0.4.0"
slip10_ed25519 = "0.1.3"

name-variant = "0.1.0"
typed-store = "0.1.0"
Expand Down
105 changes: 105 additions & 0 deletions crates/sui-types/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use std::str::FromStr;

use anyhow::{anyhow, Error};
use base64ct::Encoding;
use bip32::{ChildNumber, DerivationPath, XPrv};
use digest::Digest;
use fastcrypto::bls12381::BLS12381PublicKey;
use fastcrypto::ed25519::{
Expand All @@ -29,6 +30,7 @@ use serde::{Deserialize, Deserializer, Serialize};
use serde_with::{serde_as, Bytes};
use sha3::Sha3_256;
use signature::Signer;
use slip10_ed25519::derive_ed25519_private_key;

use crate::base_types::{AuthorityName, SuiAddress};
use crate::committee::{Committee, EpochId};
Expand All @@ -54,6 +56,9 @@ pub type NetworkPublicKey = Ed25519PublicKey;
pub type NetworkPrivateKey = Ed25519PrivateKey;

pub const PROOF_OF_POSSESSION_DOMAIN: &[u8] = b"kosk";
pub const DERIVATION_PATH_COIN_TYPE: u32 = 784;
pub const DERVIATION_PATH_PURPOSE_ED25519: u32 = 44;
pub const DERVIATION_PATH_PURPOSE_SECP256K1: u32 = 54;

// Creates a proof that the keypair is possesed, as well as binds this proof to a specific SuiAddress.
pub fn generate_proof_of_possession<K: KeypairTraits>(
Expand Down Expand Up @@ -489,6 +494,106 @@ where
(kp.public().into(), kp)
}

/// Ed25519 follows SLIP-0010 using hardened path: m/44'/784'/0'/0'/{index}'
/// Secp256k1 follows BIP-32 using path where the first 3 levels are hardened: m/54'/784'/0'/0/{index}
/// Note that the purpose for Secp256k1 is registered as 54, to differentiate from Ed25519 with purpose 44.
pub fn derive_key_pair_from_path(
seed: &[u8],
derivation_path: Option<DerivationPath>,
key_scheme: &SignatureScheme,
) -> Result<(SuiAddress, SuiKeyPair), SuiError> {
let path = validate_path(key_scheme, derivation_path)?;
match key_scheme {
SignatureScheme::ED25519 => {
let indexes = path.into_iter().map(|i| i.into()).collect::<Vec<_>>();
let derived = derive_ed25519_private_key(seed, &indexes);
let sk = Ed25519PrivateKey::from_bytes(&derived)
.map_err(|e| SuiError::SignatureKeyGenError(e.to_string()))?;
let kp = Ed25519KeyPair::from(sk);
Ok((kp.public().into(), SuiKeyPair::Ed25519SuiKeyPair(kp)))
}
SignatureScheme::Secp256k1 => {
let child_xprv = XPrv::derive_from_path(&seed, &path)
.map_err(|e| SuiError::SignatureKeyGenError(e.to_string()))?;
let kp = Secp256k1KeyPair::from(
Secp256k1PrivateKey::from_bytes(child_xprv.private_key().to_bytes().as_slice())
.unwrap(),
);
Ok((kp.public().into(), SuiKeyPair::Secp256k1SuiKeyPair(kp)))
}
SignatureScheme::BLS12381 => Err(SuiError::UnsupportedFeatureError {
error: "BLS is not supported for user key derivation".to_string(),
}),
}
}

pub fn validate_path(
key_scheme: &SignatureScheme,
path: Option<DerivationPath>,
) -> Result<DerivationPath, SuiError> {
match key_scheme {
SignatureScheme::ED25519 => {
match path {
Some(p) => {
// The derivation path must be hardened at all levels with purpose = 44, coin_type = 784
if let &[purpose, coin_type, account, change, address] = p.as_ref() {
if purpose
== ChildNumber::new(DERVIATION_PATH_PURPOSE_ED25519, true).unwrap()
&& coin_type
== ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).unwrap()
&& account.is_hardened()
&& change.is_hardened()
&& address.is_hardened()
{
Ok(p)
} else {
Err(SuiError::SignatureKeyGenError("Invalid path".to_string()))
}
} else {
Err(SuiError::SignatureKeyGenError("Invalid path".to_string()))
}
}
None => Ok(format!(
"m/{DERVIATION_PATH_PURPOSE_ED25519}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0'/0'"
)
.parse()
.unwrap()),
}
}
SignatureScheme::Secp256k1 => {
match path {
Some(p) => {
// The derivation path must be hardened at first 3 levels with purpose = 54, coin_type = 784
if let &[purpose, coin_type, account, change, address] = p.as_ref() {
if purpose
== ChildNumber::new(DERVIATION_PATH_PURPOSE_SECP256K1, true).unwrap()
&& coin_type
== ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).unwrap()
&& account.is_hardened()
&& !change.is_hardened()
&& !address.is_hardened()
{
Ok(p)
} else {
Err(SuiError::SignatureKeyGenError("Invalid path".to_string()))
}
} else {
Err(SuiError::SignatureKeyGenError("Invalid path".to_string()))
}
}
None => Ok(format!(
"m/{DERVIATION_PATH_PURPOSE_SECP256K1}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0/0"
)
.parse()
.unwrap()),
}
}
SignatureScheme::BLS12381 => Err(SuiError::UnsupportedFeatureError {
error: "BLS is not supported for user key derivation".to_string(),
}),
}
}

/// Wrapper function to return SuiKeypair based on key scheme string with seedable rng.
pub fn random_key_pair_by_type_from_rng<R>(
key_scheme: SignatureScheme,
Expand Down
1 change: 1 addition & 0 deletions crates/sui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ tracing = "0.1.36"
bcs = "0.1.3"
clap = { version = "3.2.17", features = ["derive"] }
telemetry-subscribers = "0.1.0"
bip32 = "0.4.0"

sui-core = { path = "../sui-core" }
sui-framework = { path = "../sui-framework" }
Expand Down
18 changes: 14 additions & 4 deletions crates/sui/src/client_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use std::{
};

use anyhow::anyhow;
use bip32::DerivationPath;
use clap::*;
use colored::Colorize;
use move_core_types::language_storage::TypeTag;
Expand Down Expand Up @@ -190,9 +191,13 @@ pub enum SuiClientCommands {
#[clap(name = "addresses")]
Addresses,

/// Generate new address and keypair with keypair scheme flag {ed25519 | secp256k1}.
/// Generate new address and keypair with keypair scheme flag {ed25519 | secp256k1}
/// with optional derivation path, default to m/44'/784'/0'/0'/0' for ed25519 or m/54'/784'/0'/0/0 for secp256k1.
#[clap(name = "new-address")]
NewAddress { key_scheme: SignatureScheme },
NewAddress {
key_scheme: SignatureScheme,
derivation_path: Option<DerivationPath>,
},

/// Obtain all objects owned by the address.
#[clap(name = "objects")]
Expand Down Expand Up @@ -409,8 +414,13 @@ impl SuiClientCommands {

SuiClientCommandResult::SyncClientState
}
SuiClientCommands::NewAddress { key_scheme } => {
let (address, phrase, scheme) = context.keystore.generate_new_key(key_scheme)?;
SuiClientCommands::NewAddress {
key_scheme,
derivation_path,
} => {
let (address, phrase, scheme) = context
.keystore
.generate_new_key(key_scheme, derivation_path)?;
SuiClientCommandResult::NewAddress((address, phrase, scheme))
}
SuiClientCommands::Gas { address } => {
Expand Down
Loading

0 comments on commit 71fd0a1

Please sign in to comment.