Skip to main content

defuse_crypto/
secp256k1.rs

1pub use k256;
2
3use k256::{
4    Sec1Point,
5    ecdsa::{RecoveryId, Signature, VerifyingKey},
6};
7
8use crate::{Curve, RecoverableCurve};
9
10/// Secp256k1 (a.k.a. k256) Elliptic Curve Digital Signature Algorithm
11pub struct Secp256k1;
12
13impl Curve for Secp256k1 {
14    type PublicKey = VerifyingKey;
15    type Signature = Signature;
16
17    /// Verify secp256k1 signature over **32-byte prehash** (i.e. output of
18    /// cryptographic hash function) for given public key.
19    #[inline]
20    fn verify(public_key: &VerifyingKey, prehash: &[u8], signature: &Self::Signature) -> bool {
21        // accept only 32 byte prehash
22        let Ok(prehash) = <&[u8; 32]>::try_from(prehash) else {
23            return false;
24        };
25
26        cfg_select! {
27            near => {
28                // `near_sdk::env::ecrecover` requires recovery_id, so
29                // we need to find one by trial recovery
30                for id in 0..=RecoveryId::MAX {
31                    let recovery_id = RecoveryId::from_byte(id).unwrap_or_else(|| unreachable!());
32
33                    if let Some(recovered) = Self::recover(prehash, signature, recovery_id)
34                        && recovered == *public_key
35                    {
36                        return true;
37                    }
38                }
39                // no recovery id was found
40                false
41            }
42            _ => {{
43                use k256::{
44                    ecdsa::signature::hazmat::PrehashVerifier,
45                    elliptic_curve::scalar::IsHigh,
46                };
47
48                if signature.s().is_high().into() {
49                    // guard against signature malleability
50                    return false;
51                }
52
53                public_key.verify_prehash(prehash, signature).is_ok()
54            }}
55        }
56    }
57}
58
59impl RecoverableCurve for Secp256k1 {
60    type RecoveryId = RecoveryId;
61
62    #[inline]
63    fn recover(
64        prehash: &[u8],
65        signature: &Self::Signature,
66        recovery_id: Self::RecoveryId,
67    ) -> Option<Self::PublicKey> {
68        // accept only 32 byte prehash
69        let prehash: &[u8; 32] = prehash.try_into().ok()?;
70
71        let public_key = {
72            cfg_select! {
73                near => {
74                    let pk: [u8; 64] = ::near_sdk::env::ecrecover(
75                        prehash,
76                        &signature.to_bytes(),
77                        recovery_id.to_byte(),
78                        // Do not accept malleable signatures:
79                        // https://github.com/near/nearcore/blob/d73041cc1d1a70af4456fceefaceb1bf7f684fde/core/crypto/src/signature.rs#L448-L455
80                        true,
81                    )?;
82
83                    Secp256k1UncompressedPublicKey(pk).try_into().ok()
84                }
85                _ => {
86                    use k256::elliptic_curve::scalar::IsHigh;
87
88                    if signature.s().is_high().into() {
89                        // guard against signature malleability
90                        return None;
91                    }
92
93                    VerifyingKey::recover_from_prehash(prehash, signature, recovery_id).ok()
94                }
95            }
96        }?;
97
98        Some(public_key)
99    }
100}
101
102#[cfg(feature = "signing")]
103const _: () = {
104    use k256::ecdsa::{Error, SigningKey};
105
106    use crate::{RecoverableSigner, Signer};
107
108    impl Signer<Secp256k1> for SigningKey {
109        type Error = Error;
110
111        #[inline]
112        fn public_key(&self) -> <Secp256k1 as Curve>::PublicKey {
113            *self.verifying_key()
114        }
115
116        /// Sign **32-byte prehash** (i.e. output of cryptographic hash
117        /// function).
118        ///
119        /// If given prehash is of different length, an error will be returned.
120        async fn sign(
121            &self,
122            prehash: &[u8],
123        ) -> Result<<Secp256k1 as Curve>::Signature, Self::Error> {
124            RecoverableSigner::sign_recoverable(self, prehash)
125                .await
126                .map(|s| s.0)
127        }
128    }
129
130    impl RecoverableSigner<Secp256k1> for SigningKey {
131        /// Sign **32-byte prehash** (i.e. output of cryptographic hash
132        /// function) and return a signature along with recovery id.
133        ///
134        /// If given prehash is of different length, an error will be returned.
135        async fn sign_recoverable(
136            &self,
137            prehash: &[u8],
138        ) -> Result<
139            (
140                <Secp256k1 as Curve>::Signature,
141                <Secp256k1 as RecoverableCurve>::RecoveryId,
142            ),
143            Self::Error,
144        > {
145            let prehash: &[u8; 32] = prehash
146                .try_into()
147                .map_err(|_| Error::from_source("prehash must be 32 bytes long"))?;
148
149            // Signature is automatically normalized to be low-S form, since
150            // `<k256::Secp256k1 as EcdsaCurve>::NORMALIZE_S` is set.
151            Ok(self.sign_prehash_recoverable(prehash))
152        }
153    }
154};
155
156/// Uncompressed Secp256k1 public key **without** leading SEC-1 tag byte.
157#[cfg_attr(
158    feature = "serde",
159    derive(::serde_with::SerializeDisplay, ::serde_with::DeserializeFromStr),
160    cfg_attr(
161        feature = "schemars-v0_8",
162        derive(::schemars::JsonSchema),
163        schemars(example = "Self::example"),
164    )
165)]
166#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))]
167#[cfg_attr(
168    feature = "borsh",
169    derive(::borsh::BorshSerialize, ::borsh::BorshDeserialize),
170    cfg_attr(feature = "borsh-schema", derive(::borsh::BorshSchema))
171)]
172#[derive(
173    Debug,
174    Clone,
175    Copy,
176    PartialEq,
177    Eq,
178    PartialOrd,
179    Ord,
180    Hash,
181    derive_more::AsRef,
182    derive_more::From,
183    derive_more::Into,
184)]
185#[as_ref([u8], [u8; 64])]
186#[into(owned, ref)]
187#[repr(transparent)]
188pub struct Secp256k1UncompressedPublicKey(
189    // schemars@0.8 ignores `with` at struct level for newtypes; must be on the field
190    #[cfg_attr(feature = "schemars-v0_8", schemars(with = "String"))] pub [u8; 64],
191);
192
193impl Secp256k1UncompressedPublicKey {
194    #[cfg(feature = "schemars-v0_8")]
195    const fn example() -> Self {
196        Self(hex_literal::hex!(
197            "85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"
198        ))
199    }
200}
201
202impl From<VerifyingKey> for Secp256k1UncompressedPublicKey {
203    #[inline]
204    fn from(value: VerifyingKey) -> Self {
205        (&value).into()
206    }
207}
208
209impl From<&VerifyingKey> for Secp256k1UncompressedPublicKey {
210    #[inline]
211    fn from(value: &VerifyingKey) -> Self {
212        Self(
213            value
214                .to_sec1_point(false) // do not compress
215                .as_bytes()[1..] // skip SEC-1 leading tag byte
216                .try_into()
217                .unwrap_or_else(|_| unreachable!()),
218        )
219    }
220}
221
222impl TryFrom<Secp256k1UncompressedPublicKey> for VerifyingKey {
223    type Error = k256::ecdsa::Error;
224
225    #[inline]
226    fn try_from(value: Secp256k1UncompressedPublicKey) -> Result<Self, Self::Error> {
227        (&value).try_into()
228    }
229}
230
231impl TryFrom<&Secp256k1UncompressedPublicKey> for VerifyingKey {
232    type Error = k256::ecdsa::Error;
233
234    #[inline]
235    fn try_from(value: &Secp256k1UncompressedPublicKey) -> Result<Self, Self::Error> {
236        Self::from_sec1_point(&Sec1Point::from_untagged_bytes((&value.0).into()))
237    }
238}
239
240/// Recoverable 65-byte secp256k1 signature, i.e. 64-byte signature with
241/// additional recovery byte
242#[cfg_attr(
243    feature = "serde",
244    derive(::serde_with::SerializeDisplay, ::serde_with::DeserializeFromStr),
245    cfg_attr(
246        feature = "schemars-v0_8",
247        derive(::schemars::JsonSchema),
248        schemars(example = "Self::example"),
249    )
250)]
251#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))]
252#[cfg_attr(
253    feature = "borsh",
254    derive(::borsh::BorshSerialize, ::borsh::BorshDeserialize),
255    cfg_attr(feature = "borsh-schema", derive(::borsh::BorshSchema))
256)]
257#[derive(
258    Debug,
259    Clone,
260    Copy,
261    PartialEq,
262    Eq,
263    PartialOrd,
264    Ord,
265    Hash,
266    derive_more::AsRef,
267    derive_more::From,
268    derive_more::Into,
269)]
270#[as_ref([u8], [u8; 65])]
271#[into(owned, ref)]
272#[repr(transparent)]
273pub struct Secp256k1RecoverableSignature(
274    // schemars@0.8 ignores `with` at struct level for newtypes; must be on the field
275    #[cfg_attr(feature = "schemars-v0_8", schemars(with = "String"))] pub [u8; 65],
276);
277
278impl Secp256k1RecoverableSignature {
279    #[cfg(feature = "schemars-v0_8")]
280    const fn example() -> Self {
281        Self(hex_literal::hex!(
282            "7800a70d05cde2c49ed546a6ce887ce6027c2c268c0285f6efef0cdfc4366b23643790f67a86468ee8301ed12cfffcb07c6530f90a9327ec057800fabd332e4701"
283        ))
284    }
285}
286
287impl From<(Signature, RecoveryId)> for Secp256k1RecoverableSignature {
288    #[inline]
289    fn from((signature, recovery_id): (Signature, RecoveryId)) -> Self {
290        (&signature, recovery_id).into()
291    }
292}
293
294impl From<(&Signature, RecoveryId)> for Secp256k1RecoverableSignature {
295    #[inline]
296    fn from((signature, recovery_id): (&Signature, RecoveryId)) -> Self {
297        let mut buf = [0u8; 65];
298        buf[..64].copy_from_slice(&signature.to_bytes());
299        buf[64] = recovery_id.to_byte();
300        Self(buf)
301    }
302}
303
304impl TryFrom<Secp256k1RecoverableSignature> for (Signature, RecoveryId) {
305    type Error = k256::ecdsa::Error;
306
307    #[inline]
308    fn try_from(value: Secp256k1RecoverableSignature) -> Result<Self, Self::Error> {
309        (&value).try_into()
310    }
311}
312
313impl TryFrom<Secp256k1RecoverableSignature> for Signature {
314    type Error = k256::ecdsa::Error;
315
316    #[inline]
317    fn try_from(value: Secp256k1RecoverableSignature) -> Result<Self, Self::Error> {
318        (&value).try_into()
319    }
320}
321
322impl TryFrom<&Secp256k1RecoverableSignature> for (Signature, RecoveryId) {
323    type Error = k256::ecdsa::Error;
324
325    #[inline]
326    fn try_from(value: &Secp256k1RecoverableSignature) -> Result<Self, Self::Error> {
327        let [ref signature @ .., recovery_id] = value.0;
328        Ok((
329            Signature::from_bytes(signature.into())?,
330            RecoveryId::from_byte(recovery_id).ok_or_else(k256::ecdsa::Error::new)?,
331        ))
332    }
333}
334
335impl TryFrom<&Secp256k1RecoverableSignature> for Signature {
336    type Error = k256::ecdsa::Error;
337
338    #[inline]
339    fn try_from(value: &Secp256k1RecoverableSignature) -> Result<Self, Self::Error> {
340        <(Self, RecoveryId)>::try_from(value).map(|t| t.0)
341    }
342}
343
344#[cfg(feature = "fmt")]
345const _: () = {
346    use core::{
347        fmt::{self, Display},
348        str::FromStr,
349    };
350
351    use crate::fmt::{ParseCurveError, TypedCurve};
352
353    impl TypedCurve for Secp256k1 {
354        const CURVE_TYPE: &str = "secp256k1";
355    }
356
357    impl Display for Secp256k1UncompressedPublicKey {
358        #[inline]
359        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
360            f.write_str(&Secp256k1::to_base58(self.0))
361        }
362    }
363
364    impl FromStr for Secp256k1UncompressedPublicKey {
365        type Err = ParseCurveError;
366
367        #[inline]
368        fn from_str(s: &str) -> Result<Self, Self::Err> {
369            Secp256k1::parse_base58(s).map(Self)
370        }
371    }
372
373    impl Display for Secp256k1RecoverableSignature {
374        #[inline]
375        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
376            f.write_str(&Secp256k1::to_base58(self.0))
377        }
378    }
379
380    impl FromStr for Secp256k1RecoverableSignature {
381        type Err = ParseCurveError;
382
383        #[inline]
384        fn from_str(s: &str) -> Result<Self, Self::Err> {
385            Secp256k1::parse_base58(s).map(Self)
386        }
387    }
388};
389
390#[cfg(test)]
391mod tests {
392    use hex_literal::hex;
393    use k256::{ecdsa::SigningKey, elliptic_curve::Generate};
394    use rstest::rstest;
395
396    use crate::tests::test_sign_recover;
397
398    use super::*;
399
400    #[rstest]
401    #[case(
402        hex!("85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"),
403        hex!("aa05af77f274774b8bdc7b61d98bc40da523dc2821fdea555f4d6aa413199bcc"),
404        hex!("7800a70d05cde2c49ed546a6ce887ce6027c2c268c0285f6efef0cdfc4366b23643790f67a86468ee8301ed12cfffcb07c6530f90a9327ec057800fabd332e4701"),
405    )]
406    #[case(
407        hex!("85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"),
408        hex!("1632c0ebba467e157675403ba3ba280b836e1801b5678d878dfc90bfc403d6e1"),
409        hex!("eea1651a60600ec4d9c45e8ae81da1a78377f789f0ac2019de66ad943459913015ef9256809ee0e6bb76e303a0b4802e475c1d26ade5d585292b80c9fe9cb10c01"),
410    )]
411    fn verify_ok(
412        #[case] public_key: impl Into<Secp256k1UncompressedPublicKey>,
413        #[case] prehash: [u8; 32],
414        #[case] signature: impl Into<Secp256k1RecoverableSignature>,
415    ) {
416        assert!(
417            Secp256k1::verify(
418                &public_key.into().try_into().unwrap(),
419                &prehash,
420                &signature.into().try_into().unwrap(),
421            ),
422            "signature is invalid",
423        );
424    }
425
426    #[rstest]
427    #[case(
428        hex!("85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"),
429        hex!("1632c0ebba467e157675403ba3ba280b836e1801b5678d878dfc90bfc403d6e1"),
430        hex!("7800a70d05cde2c49ed546a6ce887ce6027c2c268c0285f6efef0cdfc4366b23643790f67a86468ee8301ed12cfffcb07c6530f90a9327ec057800fabd332e4701"),
431    )]
432    #[case(
433        hex!("85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"),
434        hex!("aa05af77f274774b8bdc7b61d98bc40da523dc2821fdea555f4d6aa413199bcc"),
435        hex!("eea1651a60600ec4d9c45e8ae81da1a78377f789f0ac2019de66ad943459913015ef9256809ee0e6bb76e303a0b4802e475c1d26ade5d585292b80c9fe9cb10c01"),
436    )]
437    fn verify_fail(
438        #[case] public_key: impl Into<Secp256k1UncompressedPublicKey>,
439        #[case] prehash: [u8; 32],
440        #[case] signature: impl Into<Secp256k1RecoverableSignature>,
441    ) {
442        assert!(
443            !Secp256k1::verify(
444                &public_key.into().try_into().unwrap(),
445                &prehash,
446                &signature.into().try_into().unwrap(),
447            ),
448            "invalid signature passed verification",
449        );
450    }
451
452    #[rstest]
453    #[case(
454        hex!("85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"),
455        hex!("aa05af77f274774b8bdc7b61d98bc40da523dc2821fdea555f4d6aa413199bcc"),
456        hex!("7800a70d05cde2c49ed546a6ce887ce6027c2c268c0285f6efef0cdfc4366b23643790f67a86468ee8301ed12cfffcb07c6530f90a9327ec057800fabd332e4701"),
457    )]
458    #[case(
459        hex!("85a66984273f338ce4ef7b85e5430b008307e8591bb7c1b980852cf6423770b801f41e9438155eb53a5e20f748640093bb42ae3aeca035f7b7fd7a1a21f22f68"),
460        hex!("1632c0ebba467e157675403ba3ba280b836e1801b5678d878dfc90bfc403d6e1"),
461        hex!("eea1651a60600ec4d9c45e8ae81da1a78377f789f0ac2019de66ad943459913015ef9256809ee0e6bb76e303a0b4802e475c1d26ade5d585292b80c9fe9cb10c01"),
462    )]
463    fn recover_ok(
464        #[case] public_key: impl Into<Secp256k1UncompressedPublicKey>,
465        #[case] prehash: [u8; 32],
466        #[case] signature: impl Into<Secp256k1RecoverableSignature>,
467    ) {
468        let (signature, recovery_id) = signature.into().try_into().unwrap();
469
470        assert_eq!(
471            Secp256k1::recover(&prehash, &signature, recovery_id),
472            Some(public_key.into().try_into().unwrap()),
473            "invalid recovered public key",
474        );
475    }
476
477    #[rstest]
478    #[case(
479        hex!("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
480    )]
481    #[case(
482        hex!("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"),
483    )]
484    #[tokio::test]
485    async fn sign_recover(#[case] prehash: [u8; 32]) {
486        test_sign_recover(SigningKey::generate(), prehash).await;
487    }
488}