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