Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: use SLIP10 for ed25519 key derivation for ts and cli #4524

Merged
merged 1 commit into from
Sep 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]}
joyqvq marked this conversation as resolved.
Show resolved Hide resolved
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> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in a follow up pr, this test can be merged with keytool_test (this test class can be deprecated)

// 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::{
BLS12381AggregateSignature, BLS12381KeyPair, BLS12381PrivateKey, BLS12381PublicKey,
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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<3

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 I don't quite agree to use 54 for Secp256k1 if we follow bip44.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@5kbpers what do you suggest?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kchalkias IMO just using 44 is fine, otherwise some developers may be confused that we did not follow the standard.

Copy link
Collaborator

@kchalkias kchalkias Sep 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@5kbpers We need a way to distinguish between different key types. We realized other chains that support multiple schemes do that as well (usually for multi-sig), and we also picked 54 because there is no BIP54 (to avoid causing any confusion). Note that down the line Sui will support many key types and wallets should pick their favorite, but we need consistency on the deterministic derivation to allow for key (mnemonics) working with more than one vendors.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ed25519 uses purpose as 44 and it would be the recommended signing scheme.
we want to explicitly use a different derivation path for each signature scheme, and purpose level would be the most appropriate to use.

while it's non-standard to use purpose 54 for bip44, it's used by other currencies such as bitcoin to signify different signature schemes using purpose as something else (https://github.com/trezor/trezor-firmware/blob/master/docs/misc/coins-bip44-paths.md#notes ) also eth2 BLS validator key derivation. (https://eips.ethereum.org/EIPS/eip-2334)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joyqvq @kchalkias Thanks for your kind explanation! Understood what we were concerned about, now I think it looks good to me.

pub fn derive_key_pair_from_path(
seed: &[u8],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In a future PR let's have the seed as a distinct struct vs plain [u8]. Fine to merge as it is now; this will be a generic crypto refactoring.

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 => {
joyqvq marked this conversation as resolved.
Show resolved Hide resolved
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