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