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,
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((kp.public().into(), IotaKeyPair::Ed25519(kp)))
45        }
46        SignatureScheme::Secp256k1 => {
47            let child_xprv = XPrv::derive_from_path(seed, &path)
48                .map_err(|e| IotaError::SignatureKeyGen(e.to_string()))?;
49            let kp = Secp256k1KeyPair::from(
50                Secp256k1PrivateKey::from_bytes(child_xprv.private_key().to_bytes().as_slice())
51                    .map_err(|e| IotaError::SignatureKeyGen(e.to_string()))?,
52            );
53            Ok((kp.public().into(), IotaKeyPair::Secp256k1(kp)))
54        }
55        SignatureScheme::Secp256r1 => {
56            let child_xprv = XPrv::derive_from_path(seed, &path)
57                .map_err(|e| IotaError::SignatureKeyGen(e.to_string()))?;
58            let kp = Secp256r1KeyPair::from(
59                Secp256r1PrivateKey::from_bytes(child_xprv.private_key().to_bytes().as_slice())
60                    .map_err(|e| IotaError::SignatureKeyGen(e.to_string()))?,
61            );
62            Ok((kp.public().into(), IotaKeyPair::Secp256r1(kp)))
63        }
64        SignatureScheme::BLS12381
65        | SignatureScheme::MultiSig
66        | SignatureScheme::ZkLoginAuthenticator
67        | SignatureScheme::PasskeyAuthenticator
68        | SignatureScheme::MoveAuthenticator => Err(IotaError::UnsupportedFeature {
69            error: format!("key derivation not supported {key_scheme:?}"),
70        }),
71    }
72}
73
74pub fn validate_path(
75    key_scheme: &SignatureScheme,
76    path: Option<DerivationPath>,
77) -> Result<DerivationPath, IotaError> {
78    match key_scheme {
79        SignatureScheme::ED25519 => {
80            match path {
81                Some(p) => {
82                    // The derivation path must be hardened at all levels with purpose = 44,
83                    // coin_type = 4218
84                    if let &[purpose, coin_type, account, change, address] = p.as_ref() {
85                        if Some(purpose)
86                            == ChildNumber::new(DERVIATION_PATH_PURPOSE_ED25519, true).ok()
87                            && (Some(coin_type)
88                                == ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).ok())
89                            && account.is_hardened()
90                            && change.is_hardened()
91                            && address.is_hardened()
92                        {
93                            Ok(p)
94                        } else {
95                            Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
96                        }
97                    } else {
98                        Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
99                    }
100                }
101                None => Ok(format!(
102                    "m/{DERVIATION_PATH_PURPOSE_ED25519}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0'/0'"
103                )
104                .parse()
105                .map_err(|_| IotaError::SignatureKeyGen("Cannot parse path".to_string()))?),
106            }
107        }
108        SignatureScheme::Secp256k1 => {
109            match path {
110                Some(p) => {
111                    // The derivation path must be hardened at first 3 levels with purpose = 54,
112                    // coin_type = 4218
113                    if let &[purpose, coin_type, account, change, address] = p.as_ref() {
114                        if Some(purpose)
115                            == ChildNumber::new(DERVIATION_PATH_PURPOSE_SECP256K1, true).ok()
116                            && Some(coin_type)
117                                == ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).ok()
118                            && account.is_hardened()
119                            && !change.is_hardened()
120                            && !address.is_hardened()
121                        {
122                            Ok(p)
123                        } else {
124                            Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
125                        }
126                    } else {
127                        Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
128                    }
129                }
130                None => Ok(format!(
131                    "m/{DERVIATION_PATH_PURPOSE_SECP256K1}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0/0"
132                )
133                .parse()
134                .map_err(|_| IotaError::SignatureKeyGen("Cannot parse path".to_string()))?),
135            }
136        }
137        SignatureScheme::Secp256r1 => {
138            match path {
139                Some(p) => {
140                    // The derivation path must be hardened at first 3 levels with purpose = 74,
141                    // coin_type = 4218
142                    if let &[purpose, coin_type, account, change, address] = p.as_ref() {
143                        if Some(purpose)
144                            == ChildNumber::new(DERVIATION_PATH_PURPOSE_SECP256R1, true).ok()
145                            && Some(coin_type)
146                                == ChildNumber::new(DERIVATION_PATH_COIN_TYPE, true).ok()
147                            && account.is_hardened()
148                            && !change.is_hardened()
149                            && !address.is_hardened()
150                        {
151                            Ok(p)
152                        } else {
153                            Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
154                        }
155                    } else {
156                        Err(IotaError::SignatureKeyGen("Invalid path".to_string()))
157                    }
158                }
159                None => Ok(format!(
160                    "m/{DERVIATION_PATH_PURPOSE_SECP256R1}'/{DERIVATION_PATH_COIN_TYPE}'/0'/0/0"
161                )
162                .parse()
163                .map_err(|_| IotaError::SignatureKeyGen("Cannot parse path".to_string()))?),
164            }
165        }
166        SignatureScheme::BLS12381
167        | SignatureScheme::MultiSig
168        | SignatureScheme::ZkLoginAuthenticator
169        | SignatureScheme::PasskeyAuthenticator
170        | SignatureScheme::MoveAuthenticator => Err(IotaError::UnsupportedFeature {
171            error: format!("key derivation not supported {key_scheme:?}"),
172        }),
173    }
174}
175
176pub fn generate_new_key(
177    key_scheme: SignatureScheme,
178    derivation_path: Option<DerivationPath>,
179    word_length: Option<String>,
180) -> Result<(IotaAddress, IotaKeyPair, SignatureScheme, String), anyhow::Error> {
181    let mnemonic = Mnemonic::new(parse_word_length(word_length)?, Language::English);
182    let seed = Seed::new(&mnemonic, "");
183    match derive_key_pair_from_path(seed.as_bytes(), derivation_path, &key_scheme) {
184        Ok((address, kp)) => Ok((address, kp, key_scheme, mnemonic.phrase().to_string())),
185        Err(e) => Err(anyhow!("Failed to generate keypair: {:?}", e)),
186    }
187}
188
189fn parse_word_length(s: Option<String>) -> Result<MnemonicType, anyhow::Error> {
190    match s {
191        None => Ok(MnemonicType::Words12),
192        Some(s) => match s.as_str() {
193            "word12" => Ok(MnemonicType::Words12),
194            "word15" => Ok(MnemonicType::Words15),
195            "word18" => Ok(MnemonicType::Words18),
196            "word21" => Ok(MnemonicType::Words21),
197            "word24" => Ok(MnemonicType::Words24),
198            _ => anyhow::bail!("Invalid word length"),
199        },
200    }
201}