Skip to main content

iota_keys/
key_derive.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use anyhow::anyhow;
6use bip32::{ChildNumber, DerivationPath, XPrv};
7use bip39::{Language, Mnemonic, MnemonicType, Seed};
8use fastcrypto::{
9    ed25519::{Ed25519KeyPair, Ed25519PrivateKey},
10    secp256k1::{Secp256k1KeyPair, Secp256k1PrivateKey},
11    secp256r1::{Secp256r1KeyPair, Secp256r1PrivateKey},
12    traits::{KeyPair, ToFromBytes},
13};
14use iota_sdk_types::Address;
15use iota_types::{
16    base_types::address_from_iota_pub_key,
17    crypto::{IotaKeyPair, SignatureScheme},
18    error::IotaError,
19};
20use slip10_ed25519::derive_ed25519_private_key;
21
22pub const DERIVATION_PATH_COIN_TYPE: u32 = 4218;
23pub const DERVIATION_PATH_PURPOSE_ED25519: u32 = 44;
24pub const DERVIATION_PATH_PURPOSE_SECP256K1: u32 = 54;
25pub const DERVIATION_PATH_PURPOSE_SECP256R1: u32 = 74;
26
27/// Ed25519 follows SLIP-0010 using hardened path: m/44'/4218'/0'/0'/{index}'
28/// Secp256k1 follows BIP-32/44 using path where the first 3 levels are
29/// hardened: m/54'/4218'/0'/0/{index} Secp256r1 follows BIP-32/44 using path
30/// where the first 3 levels are hardened: m/74'/4218'/0'/0/{index}.
31/// Note that the purpose node is used to distinguish signature schemes.
32pub fn derive_key_pair_from_path(
33    seed: &[u8],
34    derivation_path: Option<DerivationPath>,
35    key_scheme: &SignatureScheme,
36) -> Result<(Address, IotaKeyPair), IotaError> {
37    let path = validate_path(key_scheme, derivation_path)?;
38    match key_scheme {
39        SignatureScheme::ED25519 => {
40            let indexes = path.into_iter().map(|i| i.into()).collect::<Vec<_>>();
41            let derived = derive_ed25519_private_key(seed, &indexes);
42            let sk = Ed25519PrivateKey::from_bytes(&derived)
43                .map_err(|e| IotaError::SignatureKeyGen(e.to_string()))?;
44            let kp: Ed25519KeyPair = sk.into();
45            Ok((
46                address_from_iota_pub_key(kp.public()),
47                IotaKeyPair::Ed25519(kp),
48            ))
49        }
50        SignatureScheme::Secp256k1 => {
51            let child_xprv = XPrv::derive_from_path(seed, &path)
52                .map_err(|e| IotaError::SignatureKeyGen(e.to_string()))?;
53            let kp = Secp256k1KeyPair::from(
54                Secp256k1PrivateKey::from_bytes(child_xprv.private_key().to_bytes().as_slice())
55                    .map_err(|e| IotaError::SignatureKeyGen(e.to_string()))?,
56            );
57            Ok((
58                address_from_iota_pub_key(kp.public()),
59                IotaKeyPair::Secp256k1(kp),
60            ))
61        }
62        SignatureScheme::Secp256r1 => {
63            let child_xprv = XPrv::derive_from_path(seed, &path)
64                .map_err(|e| IotaError::SignatureKeyGen(e.to_string()))?;
65            let kp = Secp256r1KeyPair::from(
66                Secp256r1PrivateKey::from_bytes(child_xprv.private_key().to_bytes().as_slice())
67                    .map_err(|e| IotaError::SignatureKeyGen(e.to_string()))?,
68            );
69            Ok((
70                address_from_iota_pub_key(kp.public()),
71                IotaKeyPair::Secp256r1(kp),
72            ))
73        }
74        #[allow(deprecated)]
75        SignatureScheme::BLS12381
76        | SignatureScheme::MultiSig
77        | SignatureScheme::ZkLoginAuthenticatorDeprecated
78        | SignatureScheme::PasskeyAuthenticator
79        | SignatureScheme::MoveAuthenticator => Err(IotaError::UnsupportedFeature {
80            error: format!("key derivation not supported {key_scheme:?}"),
81        }),
82    }
83}
84
85pub fn validate_path(
86    key_scheme: &SignatureScheme,
87    path: Option<DerivationPath>,
88) -> Result<DerivationPath, IotaError> {
89    match key_scheme {
90        SignatureScheme::ED25519 => {
91            match path {
92                Some(p) => {
93                    // The derivation path must be hardened at all levels with purpose = 44,
94                    // coin_type = 4218
95                    if let &[purpose, coin_type, account, change, address] = p.as_ref() {
96                        if Some(purpose)
97                            == ChildNumber::new(DERVIATION_PATH_PURPOSE_ED25519, true).ok()
98                            && (Some(coin_type)
99                                == ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).ok())
100                            && account.is_hardened()
101                            && change.is_hardened()
102                            && address.is_hardened()
103                        {
104                            Ok(p)
105                        } else {
106                            Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
107                        }
108                    } else {
109                        Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
110                    }
111                }
112                None => Ok(format!(
113                    "m/{DERVIATION_PATH_PURPOSE_ED25519}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0'/0'"
114                )
115                .parse()
116                .map_err(|_| IotaError::SignatureKeyGen("Cannot parse path".to_string()))?),
117            }
118        }
119        SignatureScheme::Secp256k1 => {
120            match path {
121                Some(p) => {
122                    // The derivation path must be hardened at first 3 levels with purpose = 54,
123                    // coin_type = 4218
124                    if let &[purpose, coin_type, account, change, address] = p.as_ref() {
125                        if Some(purpose)
126                            == ChildNumber::new(DERVIATION_PATH_PURPOSE_SECP256K1, true).ok()
127                            && Some(coin_type)
128                                == ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).ok()
129                            && account.is_hardened()
130                            && !change.is_hardened()
131                            && !address.is_hardened()
132                        {
133                            Ok(p)
134                        } else {
135                            Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
136                        }
137                    } else {
138                        Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
139                    }
140                }
141                None => Ok(format!(
142                    "m/{DERVIATION_PATH_PURPOSE_SECP256K1}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0/0"
143                )
144                .parse()
145                .map_err(|_| IotaError::SignatureKeyGen("Cannot parse path".to_string()))?),
146            }
147        }
148        SignatureScheme::Secp256r1 => {
149            match path {
150                Some(p) => {
151                    // The derivation path must be hardened at first 3 levels with purpose = 74,
152                    // coin_type = 4218
153                    if let &[purpose, coin_type, account, change, address] = p.as_ref() {
154                        if Some(purpose)
155                            == ChildNumber::new(DERVIATION_PATH_PURPOSE_SECP256R1, true).ok()
156                            && Some(coin_type)
157                                == ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).ok()
158                            && account.is_hardened()
159                            && !change.is_hardened()
160                            && !address.is_hardened()
161                        {
162                            Ok(p)
163                        } else {
164                            Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
165                        }
166                    } else {
167                        Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
168                    }
169                }
170                None => Ok(format!(
171                    "m/{DERVIATION_PATH_PURPOSE_SECP256R1}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0/0"
172                )
173                .parse()
174                .map_err(|_| IotaError::SignatureKeyGen("Cannot parse path".to_string()))?),
175            }
176        }
177        #[allow(deprecated)]
178        SignatureScheme::BLS12381
179        | SignatureScheme::MultiSig
180        | SignatureScheme::ZkLoginAuthenticatorDeprecated
181        | SignatureScheme::PasskeyAuthenticator
182        | SignatureScheme::MoveAuthenticator => Err(IotaError::UnsupportedFeature {
183            error: format!("key derivation not supported {key_scheme:?}"),
184        }),
185    }
186}
187
188pub fn generate_new_key(
189    key_scheme: SignatureScheme,
190    derivation_path: Option<DerivationPath>,
191    word_length: Option<String>,
192) -> Result<(Address, IotaKeyPair, SignatureScheme, String), anyhow::Error> {
193    let mnemonic = Mnemonic::new(parse_word_length(word_length)?, Language::English);
194    let seed = Seed::new(&mnemonic, "");
195    match derive_key_pair_from_path(seed.as_bytes(), derivation_path, &key_scheme) {
196        Ok((address, kp)) => Ok((address, kp, key_scheme, mnemonic.phrase().to_string())),
197        Err(e) => Err(anyhow!("Failed to generate keypair: {e:?}")),
198    }
199}
200
201fn parse_word_length(s: Option<String>) -> Result<MnemonicType, anyhow::Error> {
202    match s {
203        None => Ok(MnemonicType::Words12),
204        Some(s) => match s.as_str() {
205            "word12" => Ok(MnemonicType::Words12),
206            "word15" => Ok(MnemonicType::Words15),
207            "word18" => Ok(MnemonicType::Words18),
208            "word21" => Ok(MnemonicType::Words21),
209            "word24" => Ok(MnemonicType::Words24),
210            _ => anyhow::bail!("Invalid word length"),
211        },
212    }
213}