iota_sdk_crypto/
lib.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5#![cfg_attr(doc_cfg, feature(doc_cfg))]
6
7use iota_types::{PersonalMessage, Transaction, UserSignature};
8pub use signature::{Error as SignatureError, Signer, Verifier};
9
10/// Error type for private key encoding/decoding operations
11#[derive(thiserror::Error, Debug)]
12#[non_exhaustive]
13pub enum PrivateKeyError {
14    /// Empty input data
15    #[error("empty data: {0}")]
16    EmptyData(String),
17    /// Invalid signature scheme
18    #[error("invalid signature scheme: {0}")]
19    InvalidScheme(String),
20    /// Bech32 encoding/decoding error
21    #[error("bech32 error: {0}")]
22    Bech32(String),
23    /// HRP (Human Readable Part) error
24    #[error("bech32 HRP error: {0}")]
25    Bech32Hrp(String),
26    #[cfg(feature = "mnemonic")]
27    #[error("mnemonic error: {0}")]
28    Bip32(#[from] bip32::Error),
29    #[cfg(feature = "mnemonic")]
30    #[error("mnemonic error: {0}")]
31    Bip39(#[from] bip39::Error),
32}
33
34#[cfg(feature = "bls12381")]
35#[cfg_attr(doc_cfg, doc(cfg(feature = "bls12381")))]
36pub mod bls12381;
37
38#[cfg(feature = "bls12381")]
39#[cfg_attr(doc_cfg, doc(cfg(feature = "bls12381")))]
40pub mod validator;
41
42#[cfg(feature = "ed25519")]
43#[cfg_attr(doc_cfg, doc(cfg(feature = "ed25519")))]
44pub mod ed25519;
45
46#[cfg(feature = "secp256k1")]
47#[cfg_attr(doc_cfg, doc(cfg(feature = "secp256k1")))]
48pub mod secp256k1;
49
50#[cfg(feature = "secp256r1")]
51#[cfg_attr(doc_cfg, doc(cfg(feature = "secp256r1")))]
52pub mod secp256r1;
53
54#[cfg(feature = "passkey")]
55#[cfg_attr(doc_cfg, doc(cfg(feature = "passkey")))]
56pub mod passkey;
57
58#[cfg(feature = "zklogin")]
59#[cfg_attr(doc_cfg, doc(cfg(feature = "zklogin")))]
60pub mod zklogin;
61
62#[cfg(any(
63    feature = "ed25519",
64    feature = "secp256r1",
65    feature = "secp256k1",
66    feature = "zklogin"
67))]
68#[cfg_attr(
69    doc_cfg,
70    doc(cfg(any(
71        feature = "ed25519",
72        feature = "secp256r1",
73        feature = "secp256k1",
74        feature = "zklogin"
75    )))
76)]
77pub mod simple;
78
79#[cfg(any(
80    feature = "ed25519",
81    feature = "secp256r1",
82    feature = "secp256k1",
83    feature = "zklogin"
84))]
85#[cfg_attr(
86    doc_cfg,
87    doc(cfg(any(
88        feature = "ed25519",
89        feature = "secp256r1",
90        feature = "secp256k1",
91        feature = "zklogin"
92    )))
93)]
94pub mod multisig;
95
96#[cfg(any(
97    feature = "ed25519",
98    feature = "secp256r1",
99    feature = "secp256k1",
100    feature = "zklogin"
101))]
102#[cfg_attr(
103    doc_cfg,
104    doc(cfg(any(
105        feature = "ed25519",
106        feature = "secp256r1",
107        feature = "secp256k1",
108        feature = "zklogin"
109    )))
110)]
111#[doc(inline)]
112pub use multisig::UserSignatureVerifier;
113
114/// Interface for signing user transactions and messages in IOTA
115///
116/// # Note
117///
118/// There is a blanket implementation of `IotaSigner` for all `T` where `T:
119/// `[`Signer`]`<`[`UserSignature`]`>` so it is generally recommended for a
120/// signer to implement `Signer<UserSignature>` and rely on the blanket
121/// implementation which handles the proper construction of the signing message.
122pub trait IotaSigner {
123    fn sign_transaction(&self, transaction: &Transaction) -> Result<UserSignature, SignatureError>;
124    fn sign_personal_message(
125        &self,
126        message: &PersonalMessage<'_>,
127    ) -> Result<UserSignature, SignatureError>;
128}
129
130impl<T: Signer<UserSignature>> IotaSigner for T {
131    fn sign_transaction(&self, transaction: &Transaction) -> Result<UserSignature, SignatureError> {
132        let msg = transaction.signing_digest();
133        self.try_sign(&msg)
134    }
135
136    fn sign_personal_message(
137        &self,
138        message: &PersonalMessage<'_>,
139    ) -> Result<UserSignature, SignatureError> {
140        let msg = message.signing_digest();
141        self.try_sign(&msg)
142    }
143}
144
145/// Interface for verifying user transactions and messages in IOTA
146///
147/// # Note
148///
149/// There is a blanket implementation of `IotaVerifier` for all `T` where `T:
150/// `[`Verifier`]`<`[`UserSignature`]`>` so it is generally recommended for a
151/// signer to implement `Verifier<UserSignature>` and rely on the blanket
152/// implementation which handles the proper construction of the signing message.
153pub trait IotaVerifier {
154    fn verify_transaction(
155        &self,
156        transaction: &Transaction,
157        signature: &UserSignature,
158    ) -> Result<(), SignatureError>;
159    fn verify_personal_message(
160        &self,
161        message: &PersonalMessage<'_>,
162        signature: &UserSignature,
163    ) -> Result<(), SignatureError>;
164}
165
166impl<T: Verifier<UserSignature>> IotaVerifier for T {
167    fn verify_transaction(
168        &self,
169        transaction: &Transaction,
170        signature: &UserSignature,
171    ) -> Result<(), SignatureError> {
172        let message = transaction.signing_digest();
173        self.verify(&message, signature)
174    }
175
176    fn verify_personal_message(
177        &self,
178        message: &PersonalMessage<'_>,
179        signature: &UserSignature,
180    ) -> Result<(), SignatureError> {
181        let message = message.signing_digest();
182        self.verify(&message, signature)
183    }
184}
185
186/// Bech32 prefix for IOTA private keys
187#[cfg(feature = "bech32")]
188#[cfg_attr(doc_cfg, doc(cfg(feature = "bech32")))]
189pub const IOTA_PRIV_KEY_PREFIX: &str = "iotaprivkey";
190
191#[cfg(feature = "mnemonic")]
192pub const DERIVATION_PATH_COIN_TYPE: u32 = 4218;
193#[cfg(feature = "mnemonic")]
194pub const DERIVATION_PATH_PURPOSE_ED25519: u32 = 44;
195#[cfg(feature = "mnemonic")]
196pub const DERIVATION_PATH_PURPOSE_SECP256K1: u32 = 54;
197#[cfg(feature = "mnemonic")]
198pub const DERIVATION_PATH_PURPOSE_SECP256R1: u32 = 74;
199
200/// Defines the scheme of a private key
201pub trait PrivateKeyScheme {
202    const SCHEME: iota_types::SignatureScheme;
203
204    /// Returns the signature scheme for this private key
205    fn scheme(&self) -> iota_types::SignatureScheme {
206        Self::SCHEME
207    }
208}
209
210/// Defines a type which can be constructed from bytes
211pub trait ToFromBytes {
212    type Error;
213    type ByteArray;
214
215    /// Returns the raw bytes as a byte array.
216    fn to_bytes(&self) -> Self::ByteArray;
217
218    /// Create an instance from raw bytes
219    fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error>
220    where
221        Self: Sized;
222}
223
224/// Defines a type that can be converted to and from flagged bytes, i.e. bytes
225/// prepended by some variant indicator flag
226pub trait ToFromFlaggedBytes {
227    type Error;
228
229    /// Returns the bytes with the flag prepended
230    fn to_flagged_bytes(&self) -> Vec<u8>;
231
232    /// Creates an instance from bytes that include the flag
233    fn from_flagged_bytes(bytes: &[u8]) -> Result<Self, Self::Error>
234    where
235        Self: Sized;
236}
237
238impl<T: ToFromBytes<Error = PrivateKeyError> + PrivateKeyScheme> ToFromFlaggedBytes for T
239where
240    T::ByteArray: AsRef<[u8]>,
241{
242    type Error = PrivateKeyError;
243
244    /// Returns the bytes with signature scheme flag prepended
245    fn to_flagged_bytes(&self) -> Vec<u8> {
246        let key_bytes = self.to_bytes();
247        let mut bytes = Vec::with_capacity(1 + key_bytes.as_ref().len());
248        bytes.push(self.scheme().to_u8());
249        bytes.extend_from_slice(key_bytes.as_ref());
250        bytes
251    }
252
253    fn from_flagged_bytes(bytes: &[u8]) -> Result<Self, Self::Error>
254    where
255        Self: Sized,
256    {
257        if bytes.is_empty() {
258            return Err(PrivateKeyError::EmptyData("flagged bytes".to_string()));
259        }
260
261        let flag = iota_types::SignatureScheme::from_byte(bytes[0])
262            .map_err(|e| PrivateKeyError::InvalidScheme(format!("{e:?}")))?;
263
264        if flag != Self::SCHEME {
265            return Err(PrivateKeyError::InvalidScheme(format!(
266                "expected {:?}, got {flag:?}",
267                Self::SCHEME
268            )));
269        }
270
271        let key_bytes = &bytes[1..];
272        Self::from_bytes(key_bytes)
273    }
274}
275
276/// Defines a type which can be converted to and from bech32 strings
277#[cfg(feature = "bech32")]
278pub trait ToFromBech32 {
279    type Error;
280
281    /// Encode this private key in Bech32 format with "iotaprivkey" prefix
282    fn to_bech32(&self) -> Result<String, Self::Error>;
283
284    /// Decode a private key from Bech32 format with "iotaprivkey" prefix
285    fn from_bech32(value: &str) -> Result<Self, Self::Error>
286    where
287        Self: Sized;
288}
289
290#[cfg(feature = "bech32")]
291impl<T: ToFromFlaggedBytes<Error = PrivateKeyError>> ToFromBech32 for T {
292    type Error = PrivateKeyError;
293
294    #[cfg(feature = "bech32")]
295    fn to_bech32(&self) -> Result<String, Self::Error> {
296        use bech32::Hrp;
297
298        let hrp = Hrp::parse(IOTA_PRIV_KEY_PREFIX)
299            .map_err(|e| PrivateKeyError::Bech32Hrp(format!("{e}")))?;
300
301        let bytes = self.to_flagged_bytes();
302
303        bech32::encode::<bech32::Bech32>(hrp, &bytes)
304            .map_err(|e| PrivateKeyError::Bech32(format!("encoding failed: {e}")))
305    }
306
307    #[cfg(feature = "bech32")]
308    fn from_bech32(value: &str) -> Result<Self, Self::Error> {
309        use bech32::Hrp;
310
311        let expected_hrp = Hrp::parse(IOTA_PRIV_KEY_PREFIX)
312            .map_err(|e| PrivateKeyError::Bech32Hrp(format!("{e}")))?;
313
314        let (hrp, data) = bech32::decode(value)
315            .map_err(|e| PrivateKeyError::Bech32(format!("decoding failed: {e}")))?;
316
317        if hrp != expected_hrp {
318            return Err(PrivateKeyError::Bech32Hrp(format!(
319                "expected {IOTA_PRIV_KEY_PREFIX}, got {hrp}"
320            )));
321        }
322
323        if data.is_empty() {
324            return Err(PrivateKeyError::EmptyData("bech32 data".to_string()));
325        }
326
327        Self::from_flagged_bytes(&data)
328    }
329}
330
331/// Defines a type which can be constructed from a mnemonic phrase
332#[cfg(feature = "mnemonic")]
333pub trait FromMnemonic {
334    type Error;
335
336    /// Create an instance from a mnemonic phrase
337    fn from_mnemonic(
338        phrase: &str,
339        account_index: impl Into<Option<u64>>,
340        password: impl Into<Option<String>>,
341    ) -> Result<Self, Self::Error>
342    where
343        Self: Sized;
344
345    /// Create an instance from a mnemonic phrase and a derivation path like:
346    /// - Ed25519: `"m/44'/4218'/0'/0'/0'"`
347    /// - Secp256k1: `"m/54'/4218'/0'/0/0"`
348    /// - Secp256r1: `"m/74'/4218'/0'/0/0"`
349    fn from_mnemonic_with_path(
350        phrase: &str,
351        path: String,
352        password: impl Into<Option<String>>,
353    ) -> Result<Self, Self::Error>
354    where
355        Self: Sized;
356}
357
358#[cfg(test)]
359mod tests {
360    use super::*;
361    use crate::{
362        ed25519::Ed25519PrivateKey, secp256k1::Secp256k1PrivateKey, secp256r1::Secp256r1PrivateKey,
363    };
364
365    #[cfg(feature = "mnemonic")]
366    #[test]
367    fn test_mnemonics_ed25519() {
368        const TEST_CASES: [[&str; 3]; 3] = [
369            [
370                "film crazy soon outside stand loop subway crumble thrive popular green nuclear struggle pistol arm wife phrase warfare march wheat nephew ask sunny firm",
371                "iotaprivkey1qrqqxhsu3ndp96644fjk4z5ams5ulgmvprklngt2jhvg2ujn5w4q2d2vplv",
372                "0x9f8e5379678525edf768d7b507dc1ba9016fc4f0eac976ab7f74077d95fba312",
373            ],
374            [
375                "require decline left thought grid priority false tiny gasp angle royal system attack beef setup reward aunt skill wasp tray vital bounce inflict level",
376                "iotaprivkey1qqcxaf57fnenvflpacacaumf6vl0rt0edddhytanvzhkqhwnjk0zspg902d",
377                "0x862738192e40540e0a5c9a5aca636f53b0cd76b0a9bef3386e05647feb4914ac",
378            ],
379            [
380                "organ crash swim stick traffic remember army arctic mesh slice swear summer police vast chaos cradle squirrel hood useless evidence pet hub soap lake",
381                "iotaprivkey1qzq39vxzm0gq7l8dc5dj5allpuww4mavhwhg8mua4cl3lj2c3fvhcv5l2vn",
382                "0x2391788ca49c7f0f00699bc2bad45f80c343b4d1df024285c132259433d7ff31",
383            ],
384        ];
385
386        for [mnemonic, bech32, address] in TEST_CASES {
387            let key = Ed25519PrivateKey::from_mnemonic(mnemonic, None, None).unwrap();
388            assert_eq!(key.to_bech32().unwrap(), bech32);
389            assert_eq!(key.public_key().derive_address().to_string(), address);
390        }
391    }
392
393    #[cfg(feature = "mnemonic")]
394    #[test]
395    fn test_mnemonics_secp256k1() {
396        const TEST_CASES: [[&str; 3]; 3] = [
397            [
398                "film crazy soon outside stand loop subway crumble thrive popular green nuclear struggle pistol arm wife phrase warfare march wheat nephew ask sunny firm",
399                "iotaprivkey1q8cy2ll8a0dmzzzwn9zavrug0qf47cyuj6k2r4r6rnjtpjhrdh52vpegd4f",
400                "0x8520d58dde1ab268349b9a46e5124ae6fe7e4c61df4ca2bc9c97d3c4d07b0b55",
401            ],
402            [
403                "require decline left thought grid priority false tiny gasp angle royal system attack beef setup reward aunt skill wasp tray vital bounce inflict level",
404                "iotaprivkey1q9hm330d05jcxfvmztv046p8kclyaj39hk6elqghgpq4sz4x23hk2wd6cfz",
405                "0x3740d570eefba29dfc0fdd5829848902064e31ecd059ca05c401907fa8646f61",
406            ],
407            [
408                "organ crash swim stick traffic remember army arctic mesh slice swear summer police vast chaos cradle squirrel hood useless evidence pet hub soap lake",
409                "iotaprivkey1qx2dnch6363h7gdqqfkzmmlequzj4ul3x4fq6dzyajk7wc2c0jgcx32axh5",
410                "0x943b852c37fef403047e06ff5a2fa216557a4386212fb29554babdd3e1899da5",
411            ],
412        ];
413
414        for [mnemonic, bech32, address] in TEST_CASES {
415            let key = Secp256k1PrivateKey::from_mnemonic(mnemonic, None, None).unwrap();
416            assert_eq!(key.to_bech32().unwrap(), bech32);
417            assert_eq!(key.public_key().derive_address().to_string(), address);
418        }
419    }
420
421    #[cfg(feature = "mnemonic")]
422    #[test]
423    fn test_mnemonics_secp256r1() {
424        const TEST_CASES: [[&str; 3]; 3] = [
425            [
426                "act wing dilemma glory episode region allow mad tourist humble muffin oblige",
427                "iotaprivkey1qtt65ua2lhal76zg4cxd6umdqynv2rj2gzrntp5rwlnyj370jg3pwtqlwdn",
428                "0x779a63b28528210a5ec6c4af5a70382fa3f0c2d3f98dcbe4e3a4ae2f8c39cc9c",
429            ],
430            [
431                "flag rebel cabbage captain minimum purpose long already valley horn enrich salt",
432                "iotaprivkey1qtcjgmue7q8u4gtutfvfpx3zj3aa2r9pqssuusrltxfv68eqhzsgjc3p4z7",
433                "0x8b45523042933aa55f57e2ccc661304baed292529b6e67a0c9857c1f3f871806",
434            ],
435            [
436                "area renew bar language pudding trial small host remind supreme cabbage era",
437                "iotaprivkey1qtxafg26qxeqy7f56gd2rvsup0a5kl4cre7nt2rtcrf0p3v5pwd4cgrrff2",
438                "0x8528ef86150ec331928a8b3edb8adbe2fb523db8c84679aa57a931da6a4cdb25",
439            ],
440        ];
441
442        for [mnemonic, bech32, address] in TEST_CASES {
443            let key = Secp256r1PrivateKey::from_mnemonic(mnemonic, None, None).unwrap();
444            assert_eq!(key.to_bech32().unwrap(), bech32);
445            assert_eq!(key.public_key().derive_address().to_string(), address);
446        }
447    }
448}