Skip to main content

subxt_signer/
ecdsa.rs

1// Copyright 2019-2026 Parity Technologies (UK) Ltd.
2// This file is dual-licensed as Apache-2.0 or GPL-3.0.
3// see LICENSE for license details.
4
5//! An ecdsa keypair implementation.
6use codec::Encode;
7
8use crate::crypto::{DeriveJunction, SecretUri, seed_from_entropy};
9use core::str::FromStr;
10use hex::FromHex;
11use secp256k1::{Message, Secp256k1, SecretKey, ecdsa::RecoverableSignature};
12use secrecy::ExposeSecret;
13
14use thiserror::Error as DeriveError;
15
16const SECRET_KEY_LENGTH: usize = 32;
17
18/// Seed bytes used to generate a key pair.
19pub type SecretKeyBytes = [u8; SECRET_KEY_LENGTH];
20
21/// A signature generated by [`Keypair::sign()`]. These bytes are equivalent
22/// to a Substrate `MultiSignature::Ecdsa(bytes)`.
23#[derive(Clone, Copy, PartialEq, Eq)]
24pub struct Signature(pub [u8; 65]);
25
26impl AsRef<[u8]> for Signature {
27    fn as_ref(&self) -> &[u8] {
28        &self.0
29    }
30}
31
32/// The (compressed) public key for an [`Keypair`] key pair.
33#[derive(Debug, Clone)]
34pub struct PublicKey(pub [u8; 33]);
35
36impl AsRef<[u8]> for PublicKey {
37    fn as_ref(&self) -> &[u8] {
38        &self.0
39    }
40}
41
42/// An ecdsa keypair implementation.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Keypair(pub secp256k1::Keypair);
45
46impl Keypair {
47    /// Create an ecdsa keypair from a [`SecretUri`]. See the [`SecretUri`] docs for more.
48    ///
49    /// # Example
50    ///
51    /// ```rust,standalone_crate
52    /// use subxt_signer::{ SecretUri, ecdsa::Keypair };
53    /// use std::str::FromStr;
54    ///
55    /// let uri = SecretUri::from_str("//Alice").unwrap();
56    /// let keypair = Keypair::from_uri(&uri).unwrap();
57    ///
58    /// keypair.sign(b"Hello world!");
59    /// ```
60    pub fn from_uri(uri: &SecretUri) -> Result<Self, Error> {
61        let SecretUri {
62            junctions,
63            phrase,
64            password,
65        } = uri;
66
67        // If the phrase is hex, convert bytes directly into a seed, ignoring password.
68        // Else, parse the phrase string taking the password into account. This is
69        // the same approach taken in sp_core::crypto::Pair::from_string_with_seed.
70        let key = if let Some(hex_str) = phrase.expose_secret().strip_prefix("0x") {
71            let seed = SecretKeyBytes::from_hex(hex_str)?;
72            Self::from_secret_key(seed)?
73        } else {
74            let phrase = bip39::Mnemonic::from_str(phrase.expose_secret())?;
75            let pass_str = password.as_ref().map(|p| p.expose_secret());
76            Self::from_phrase(&phrase, pass_str)?
77        };
78
79        // Now, use any "junctions" to derive a new key from this root key.
80        key.derive(junctions.iter().copied())
81    }
82
83    /// Create an ecdsa keypair from a BIP-39 mnemonic phrase and optional password.
84    ///
85    /// # Example
86    ///
87    /// ```rust,standalone_crate
88    /// use subxt_signer::{ bip39::Mnemonic, ecdsa::Keypair };
89    ///
90    /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
91    /// let mnemonic = Mnemonic::parse(phrase).unwrap();
92    /// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap();
93    ///
94    /// keypair.sign(b"Hello world!");
95    /// ```
96    pub fn from_phrase(mnemonic: &bip39::Mnemonic, password: Option<&str>) -> Result<Self, Error> {
97        let (arr, len) = mnemonic.to_entropy_array();
98        let big_seed =
99            seed_from_entropy(&arr[0..len], password.unwrap_or("")).ok_or(Error::InvalidSeed)?;
100
101        let secret_key_bytes: SecretKeyBytes = big_seed[..SECRET_KEY_LENGTH]
102            .try_into()
103            .expect("should be valid Seed");
104
105        Self::from_secret_key(secret_key_bytes)
106    }
107
108    /// Turn a 32 byte seed into a keypair.
109    ///
110    /// # Warning
111    ///
112    /// This will only be secure if the seed is secure!
113    pub fn from_secret_key(secret_key: SecretKeyBytes) -> Result<Self, Error> {
114        let secret = SecretKey::from_slice(&secret_key).map_err(|_| Error::InvalidSeed)?;
115        Ok(Self(secp256k1::Keypair::from_secret_key(
116            &Secp256k1::signing_only(),
117            &secret,
118        )))
119    }
120
121    /// Derive a child key from this one given a series of junctions.
122    ///
123    /// # Example
124    ///
125    /// ```rust,standalone_crate
126    /// use subxt_signer::{ bip39::Mnemonic, ecdsa::Keypair, DeriveJunction };
127    ///
128    /// let phrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk";
129    /// let mnemonic = Mnemonic::parse(phrase).unwrap();
130    /// let keypair = Keypair::from_phrase(&mnemonic, None).unwrap();
131    ///
132    /// // Equivalent to the URI path '//Alice//stash':
133    /// let new_keypair = keypair.derive([
134    ///     DeriveJunction::hard("Alice"),
135    ///     DeriveJunction::hard("stash")
136    /// ]);
137    /// ```
138    pub fn derive<Js: IntoIterator<Item = DeriveJunction>>(
139        &self,
140        junctions: Js,
141    ) -> Result<Self, Error> {
142        let mut acc = self.0.secret_key().clone().secret_bytes();
143        for junction in junctions {
144            match junction {
145                DeriveJunction::Soft(_) => return Err(Error::SoftJunction),
146                DeriveJunction::Hard(junction_bytes) => {
147                    acc = ("Secp256k1HDKD", acc, junction_bytes)
148                        .using_encoded(sp_crypto_hashing::blake2_256)
149                }
150            }
151        }
152        Self::from_secret_key(acc)
153    }
154
155    /// Obtain the [`PublicKey`] part of this key pair, which can be used in calls to [`verify()`].
156    /// or otherwise converted into an address. In case of ECDSA, the public key bytes are not
157    /// equivalent to a Substrate `AccountId32`. They have to be hashed to obtain `AccountId32`.
158    pub fn public_key(&self) -> PublicKey {
159        PublicKey(self.0.public_key().serialize())
160    }
161
162    /// Obtain the [`SecretKey`] part of this key pair. This should be kept secret.
163    pub fn secret_key(&self) -> SecretKeyBytes {
164        *self.0.secret_key().as_ref()
165    }
166
167    /// Sign some message. These bytes can be used directly in a Substrate `MultiSignature::Ecdsa(..)`.
168    pub fn sign(&self, message: &[u8]) -> Signature {
169        self.sign_prehashed(&sp_crypto_hashing::blake2_256(message))
170    }
171
172    /// Signs a pre-hashed message.
173    pub fn sign_prehashed(&self, message_hash: &[u8; 32]) -> Signature {
174        let wrapped = Message::from_digest_slice(message_hash).expect("Message is 32 bytes; qed");
175        Signature(internal::sign(&self.0.secret_key(), &wrapped))
176    }
177}
178
179/// Verify that some signature for a message was created by the owner of the [`PublicKey`].
180///
181/// ```rust,standalone_crate
182/// use subxt_signer::{ bip39::Mnemonic, ecdsa };
183///
184/// let keypair = ecdsa::dev::alice();
185/// let message = b"Hello!";
186///
187/// let signature = keypair.sign(message);
188/// let public_key = keypair.public_key();
189/// assert!(ecdsa::verify(&signature, message, &public_key));
190/// ```
191pub fn verify<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &PublicKey) -> bool {
192    let message_hash = sp_crypto_hashing::blake2_256(message.as_ref());
193    let wrapped = Message::from_digest_slice(&message_hash).expect("Message is 32 bytes; qed");
194
195    internal::verify(&sig.0, &wrapped, pubkey)
196}
197
198pub(crate) mod internal {
199    use super::*;
200
201    pub fn sign(secret_key: &secp256k1::SecretKey, message: &Message) -> [u8; 65] {
202        let recsig: RecoverableSignature =
203            Secp256k1::signing_only().sign_ecdsa_recoverable(message, secret_key);
204        let (recid, sig): (_, [u8; 64]) = recsig.serialize_compact();
205        let mut signature_bytes: [u8; 65] = [0; 65];
206        signature_bytes[..64].copy_from_slice(&sig);
207        signature_bytes[64] = (i32::from(recid) & 0xFF) as u8;
208        signature_bytes
209    }
210
211    pub fn verify(sig: &[u8; 65], message: &Message, pubkey: &PublicKey) -> bool {
212        let Ok(signature) = secp256k1::ecdsa::Signature::from_compact(&sig[..64]) else {
213            return false;
214        };
215        let Ok(public) = secp256k1::PublicKey::from_slice(&pubkey.0) else {
216            return false;
217        };
218
219        Secp256k1::verification_only()
220            .verify_ecdsa(message, &signature, &public)
221            .is_ok()
222    }
223}
224
225/// An error handed back if creating a keypair fails.
226#[derive(Debug, PartialEq, DeriveError)]
227pub enum Error {
228    /// Invalid seed.
229    #[error("Invalid seed (was it the wrong length?)")]
230    InvalidSeed,
231    /// Invalid seed.
232    #[error("Invalid seed for ECDSA, contained soft junction")]
233    SoftJunction,
234    /// Invalid phrase.
235    #[error("Cannot parse phrase: {0}")]
236    Phrase(bip39::Error),
237    /// Invalid hex.
238    #[error("Cannot parse hex string: {0}")]
239    Hex(hex::FromHexError),
240}
241
242impl From<hex::FromHexError> for Error {
243    fn from(err: hex::FromHexError) -> Self {
244        Error::Hex(err)
245    }
246}
247
248impl From<bip39::Error> for Error {
249    fn from(err: bip39::Error) -> Self {
250        Error::Phrase(err)
251    }
252}
253
254/// Dev accounts, helpful for testing but not to be used in production,
255/// since the secret keys are known.
256pub mod dev {
257    use super::*;
258
259    once_static_cloned! {
260        /// Equivalent to `{DEV_PHRASE}//Alice`.
261        pub fn alice() -> Keypair {
262            Keypair::from_uri(&SecretUri::from_str("//Alice").unwrap()).unwrap()
263        }
264        /// Equivalent to `{DEV_PHRASE}//Bob`.
265        pub fn bob() -> Keypair {
266            Keypair::from_uri(&SecretUri::from_str("//Bob").unwrap()).unwrap()
267        }
268        /// Equivalent to `{DEV_PHRASE}//Charlie`.
269        pub fn charlie() -> Keypair {
270            Keypair::from_uri(&SecretUri::from_str("//Charlie").unwrap()).unwrap()
271        }
272        /// Equivalent to `{DEV_PHRASE}//Dave`.
273        pub fn dave() -> Keypair {
274            Keypair::from_uri(&SecretUri::from_str("//Dave").unwrap()).unwrap()
275        }
276        /// Equivalent to `{DEV_PHRASE}//Eve`.
277        pub fn eve() -> Keypair {
278            Keypair::from_uri(&SecretUri::from_str("//Eve").unwrap()).unwrap()
279        }
280        /// Equivalent to `{DEV_PHRASE}//Ferdie`.
281        pub fn ferdie() -> Keypair {
282            Keypair::from_uri(&SecretUri::from_str("//Ferdie").unwrap()).unwrap()
283        }
284        /// Equivalent to `{DEV_PHRASE}//One`.
285        pub fn one() -> Keypair {
286            Keypair::from_uri(&SecretUri::from_str("//One").unwrap()).unwrap()
287        }
288        /// Equivalent to `{DEV_PHRASE}//Two`.
289        pub fn two() -> Keypair {
290            Keypair::from_uri(&SecretUri::from_str("//Two").unwrap()).unwrap()
291        }
292    }
293}
294
295// Make `Keypair` usable to sign transactions in Subxt. This is optional so that
296// `subxt-signer` can be used entirely independently of Subxt.
297#[cfg(feature = "subxt")]
298mod subxt_compat {
299    use super::*;
300    use subxt::config::Config;
301    use subxt::transactions::Signer as SignerT;
302    use subxt::utils::{AccountId32, MultiAddress, MultiSignature};
303
304    impl From<Signature> for MultiSignature {
305        fn from(value: Signature) -> Self {
306            MultiSignature::Ecdsa(value.0)
307        }
308    }
309
310    impl From<PublicKey> for AccountId32 {
311        fn from(value: PublicKey) -> Self {
312            value.to_account_id()
313        }
314    }
315
316    impl<T> From<PublicKey> for MultiAddress<AccountId32, T> {
317        fn from(value: PublicKey) -> Self {
318            value.to_address()
319        }
320    }
321
322    impl PublicKey {
323        /// A shortcut to obtain an [`AccountId32`] from a [`PublicKey`].
324        /// We often want this type, and using this method avoids any
325        /// ambiguous type resolution issues.
326        pub fn to_account_id(self) -> AccountId32 {
327            AccountId32(sp_crypto_hashing::blake2_256(&self.0))
328        }
329        /// A shortcut to obtain a [`MultiAddress`] from a [`PublicKey`].
330        /// We often want this type, and using this method avoids any
331        /// ambiguous type resolution issues.
332        pub fn to_address<T>(self) -> MultiAddress<AccountId32, T> {
333            MultiAddress::Id(self.to_account_id())
334        }
335    }
336
337    impl<T: Config> SignerT<T> for Keypair
338    where
339        T::AccountId: From<PublicKey>,
340        T::Address: From<PublicKey>,
341        T::Signature: From<Signature>,
342    {
343        fn account_id(&self) -> T::AccountId {
344            self.public_key().into()
345        }
346
347        fn sign(&self, signer_payload: &[u8]) -> T::Signature {
348            self.sign(signer_payload).into()
349        }
350    }
351}
352
353#[cfg(test)]
354mod test {
355    use std::str::FromStr;
356
357    use super::*;
358
359    use sp_core::{self, crypto::Pair as _, ecdsa::Pair as SpPair};
360
361    #[test]
362    fn check_from_phrase_matches() {
363        for _ in 0..20 {
364            let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(None);
365            let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
366            let pair = Keypair::from_phrase(&phrase, None).expect("should be valid");
367
368            assert_eq!(sp_pair.public().0, pair.public_key().0);
369        }
370    }
371
372    #[test]
373    fn check_from_phrase_with_password_matches() {
374        for _ in 0..20 {
375            let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing"));
376            let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
377            let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid");
378
379            assert_eq!(sp_pair.public().0, pair.public_key().0);
380        }
381    }
382
383    #[test]
384    fn check_from_secret_uri_matches() {
385        // Some derive junctions to check that the logic there aligns:
386        let uri_paths = ["//bar", "//0001", "//1", "//0001", "//foo//bar//wibble"];
387
388        for i in 0..2 {
389            for path in &uri_paths {
390                // Build an sp_core::Pair that includes a phrase, path and password:
391                let password = format!("Testing{i}");
392                let (_sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some(&password));
393                let uri = format!("{phrase}{path}///{password}");
394                let sp_pair = SpPair::from_string(&uri, None).expect("should be valid");
395
396                // Now build a local Keypair using the equivalent API:
397                let uri = SecretUri::from_str(&uri).expect("should be valid secret URI");
398                let pair = Keypair::from_uri(&uri).expect("should be valid");
399
400                // They should match:
401                assert_eq!(sp_pair.public().0, pair.public_key().0);
402            }
403        }
404    }
405
406    #[test]
407    fn check_derive_errs_with_soft_junction() {
408        let uri_paths = ["/bar", "/1", "//foo//bar/wibble"];
409        for path in &uri_paths {
410            let (_sp_pair, phrase, _seed) = SpPair::generate_with_phrase(None);
411            let uri = format!("{phrase}{path}");
412            let uri = SecretUri::from_str(&uri).expect("should be valid secret URI");
413            let result = Keypair::from_uri(&uri);
414            assert_eq!(result.err(), Some(Error::SoftJunction));
415        }
416    }
417
418    #[test]
419    fn check_signing_and_verifying_matches() {
420        use sp_core::ecdsa::Signature as SpSignature;
421
422        for _ in 0..20 {
423            let (sp_pair, phrase, _seed) = SpPair::generate_with_phrase(Some("Testing"));
424            let phrase = bip39::Mnemonic::parse(phrase).expect("valid phrase expected");
425            let pair = Keypair::from_phrase(&phrase, Some("Testing")).expect("should be valid");
426
427            let message = b"Hello world";
428            let sp_sig = sp_pair.sign(message).0;
429            let sig: [u8; 65] = pair.sign(message).0;
430
431            assert!(SpPair::verify(
432                &SpSignature::from(sig),
433                message,
434                &sp_pair.public(),
435            ));
436            assert!(verify(&Signature(sp_sig), message, &pair.public_key()));
437        }
438    }
439
440    #[test]
441    fn check_hex_uris() {
442        // Hex URIs seem to ignore the password on sp_core and here. Check that this is consistent.
443        let uri_str =
444            "0x1122334455667788112233445566778811223344556677881122334455667788///SomePassword";
445
446        let uri = SecretUri::from_str(uri_str).expect("should be valid");
447        let pair = Keypair::from_uri(&uri).expect("should be valid");
448        let sp_pair = SpPair::from_string(uri_str, None).expect("should be valid");
449
450        assert_eq!(pair.public_key().0, sp_pair.public().0);
451    }
452}