Skip to main content

ecdsa/
recovery.rs

1//! Public key recovery support.
2
3use crate::{Error, Result};
4
5#[cfg(feature = "algorithm")]
6use {
7    crate::{
8        DigestAlgorithm, EcdsaCurve, Signature, SigningKey, VerifyingKey,
9        hazmat::{bytes2scalar, sign_prehashed_rfc6979, verify_prehashed},
10    },
11    digest::{Digest, Update},
12    elliptic_curve::{
13        AffinePoint, CurveArithmetic, CurveGroup, FieldBytes, FieldBytesSize, PrimeField,
14        ProjectivePoint, Scalar,
15        bigint::CheckedAdd,
16        field,
17        ops::{Invert, MulByGeneratorVartime},
18        point::DecompressPoint,
19        sec1::{self, FromSec1Point, ToSec1Point},
20        subtle::CtOption,
21    },
22    signature::{
23        DigestSigner, MultipartSigner, RandomizedDigestSigner, Signer,
24        hazmat::{PrehashSigner, RandomizedPrehashSigner},
25        rand_core::TryCryptoRng,
26    },
27};
28
29/// Recovery IDs, a.k.a. "recid".
30///
31/// This is an integer value `0`, `1`, `2`, or `3` included along with a signature which is used
32/// during the recovery process to select the correct public key from the signature.
33///
34/// It consists of two bits of information:
35///
36/// 1. low bit (0/1): was the y-coordinate of the affine point resulting from the fixed-base
37///    multiplication 𝑘×𝑮 odd? This part of the algorithm functions similar to point decompression.
38/// 2. hi bit (2/3): did the affine x-coordinate of 𝑘×𝑮 overflow the order of the scalar field `n`,
39///    requiring a reduction when computing `r`?
40#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
41pub struct RecoveryId(pub(crate) u8);
42
43impl RecoveryId {
44    /// Maximum supported value for the recovery ID (inclusive).
45    pub const MAX: u8 = 3;
46
47    /// Create a new [`RecoveryId`] from the following 1-bit arguments:
48    ///
49    /// - `is_y_odd`: is the affine y-coordinate of 𝑘×𝑮 odd?
50    /// - `is_x_reduced`: did the affine x-coordinate of 𝑘×𝑮 overflow the curve order?
51    #[must_use]
52    #[allow(clippy::as_conversions, reason = "const fn")]
53    pub const fn new(is_y_odd: bool, is_x_reduced: bool) -> Self {
54        Self(((is_x_reduced as u8) << 1) | (is_y_odd as u8))
55    }
56
57    /// Did the affine x-coordinate of 𝑘×𝑮 overflow the curve order?
58    #[must_use]
59    pub const fn is_x_reduced(self) -> bool {
60        (self.0 & 0b10) != 0
61    }
62
63    /// Is the affine y-coordinate of 𝑘×𝑮 odd?
64    #[must_use]
65    pub const fn is_y_odd(self) -> bool {
66        (self.0 & 1) != 0
67    }
68
69    /// Convert a `u8` into a [`RecoveryId`].
70    #[must_use]
71    pub const fn from_byte(byte: u8) -> Option<Self> {
72        if byte <= Self::MAX {
73            Some(Self(byte))
74        } else {
75            None
76        }
77    }
78
79    /// Convert this [`RecoveryId`] into a `u8`.
80    #[must_use]
81    pub const fn to_byte(self) -> u8 {
82        self.0
83    }
84}
85
86#[cfg(feature = "algorithm")]
87impl RecoveryId {
88    /// Given a public key, message, and signature, use trial recovery to determine if a suitable
89    /// recovery ID exists.
90    ///
91    /// # Errors
92    /// Returns an error if a suitable solution could not be found and/or the signature does not
93    /// verify.
94    pub fn trial_recovery_from_msg<C>(
95        verifying_key: &VerifyingKey<C>,
96        msg: &[u8],
97        signature: &Signature<C>,
98    ) -> Result<Self>
99    where
100        C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
101        AffinePoint<C>: DecompressPoint<C> + FromSec1Point<C> + ToSec1Point<C>,
102        FieldBytesSize<C>: sec1::ModulusSize,
103    {
104        Self::trial_recovery_from_digest(verifying_key, C::Digest::new_with_prefix(msg), signature)
105    }
106
107    /// Given a public key, message digest, and signature, use trial recovery to determine if a
108    /// suitable recovery ID exists.
109    ///
110    /// # Errors
111    /// Returns an error if a suitable solution could not be found and/or the signature does not
112    /// verify.
113    pub fn trial_recovery_from_digest<C, D>(
114        verifying_key: &VerifyingKey<C>,
115        digest: D,
116        signature: &Signature<C>,
117    ) -> Result<Self>
118    where
119        C: EcdsaCurve + CurveArithmetic,
120        D: Digest,
121        AffinePoint<C>: DecompressPoint<C> + FromSec1Point<C> + ToSec1Point<C>,
122        FieldBytesSize<C>: sec1::ModulusSize,
123    {
124        Self::trial_recovery_from_prehash(verifying_key, &digest.finalize(), signature)
125    }
126
127    /// Given a public key, message digest, and signature, use trial recovery to determine if a
128    /// suitable recovery ID exists.
129    ///
130    /// # Errors
131    /// Returns an error if a suitable solution could not be found and/or the signature does not
132    /// verify.
133    pub fn trial_recovery_from_prehash<C>(
134        verifying_key: &VerifyingKey<C>,
135        prehash: &[u8],
136        signature: &Signature<C>,
137    ) -> Result<Self>
138    where
139        C: EcdsaCurve + CurveArithmetic,
140        AffinePoint<C>: DecompressPoint<C> + FromSec1Point<C> + ToSec1Point<C>,
141        FieldBytesSize<C>: sec1::ModulusSize,
142    {
143        // Ensure signature verifies with the provided key
144        verify_prehashed::<C>(
145            &ProjectivePoint::<C>::from(*verifying_key.as_affine()),
146            prehash,
147            signature,
148        )?;
149
150        for id in 0..=Self::MAX {
151            let recovery_id = RecoveryId(id);
152
153            if let Ok(vk) = VerifyingKey::recover_from_prehash(prehash, signature, recovery_id) {
154                if verifying_key == &vk {
155                    return Ok(recovery_id);
156                }
157            }
158        }
159
160        Err(Error::new())
161    }
162}
163
164impl TryFrom<u8> for RecoveryId {
165    type Error = Error;
166
167    fn try_from(byte: u8) -> Result<Self> {
168        Self::from_byte(byte).ok_or_else(Error::new)
169    }
170}
171
172impl From<RecoveryId> for u8 {
173    fn from(id: RecoveryId) -> u8 {
174        id.0
175    }
176}
177
178#[cfg(feature = "algorithm")]
179impl<C> SigningKey<C>
180where
181    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
182    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
183{
184    /// Sign the given message prehash, using the given rng for the RFC6979 Section 3.6 "additional
185    /// data", returning a signature and recovery ID.
186    ///
187    /// # Errors
188    ///
189    pub fn sign_prehash_recoverable_with_rng<R: TryCryptoRng + ?Sized>(
190        &self,
191        rng: &mut R,
192        prehash: &[u8],
193    ) -> core::result::Result<(Signature<C>, RecoveryId), R::Error> {
194        let mut ad = FieldBytes::<C>::default();
195        rng.try_fill_bytes(&mut ad)?;
196        Ok(sign_prehashed_rfc6979::<C, C::Digest>(
197            self.as_nonzero_scalar(),
198            prehash,
199            &ad,
200        ))
201    }
202
203    /// Sign the given message prehash, returning a signature and recovery ID.
204    pub fn sign_prehash_recoverable(&self, prehash: &[u8]) -> (Signature<C>, RecoveryId) {
205        sign_prehashed_rfc6979::<C, C::Digest>(self.as_nonzero_scalar(), prehash, b"")
206    }
207
208    /// Sign the given message digest, returning a signature and recovery ID.
209    pub fn sign_digest_recoverable<D: Digest>(&self, msg_digest: D) -> (Signature<C>, RecoveryId) {
210        self.sign_prehash_recoverable(&msg_digest.finalize())
211    }
212
213    /// Sign the given message, hashing it with the curve's default digest
214    /// function, and returning a signature and recovery ID.
215    pub fn sign_recoverable(&self, msg: &[u8]) -> (Signature<C>, RecoveryId) {
216        self.sign_digest_recoverable(C::Digest::new_with_prefix(msg))
217    }
218}
219
220#[cfg(feature = "algorithm")]
221impl<C, D> DigestSigner<D, (Signature<C>, RecoveryId)> for SigningKey<C>
222where
223    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
224    D: Digest + Update,
225    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
226{
227    fn try_sign_digest<F: Fn(&mut D) -> Result<()>>(
228        &self,
229        f: F,
230    ) -> Result<(Signature<C>, RecoveryId)> {
231        let mut digest = D::new();
232        f(&mut digest)?;
233        Ok(self.sign_digest_recoverable(digest))
234    }
235}
236
237#[cfg(feature = "algorithm")]
238impl<C> RandomizedPrehashSigner<(Signature<C>, RecoveryId)> for SigningKey<C>
239where
240    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
241    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
242{
243    fn sign_prehash_with_rng<R: TryCryptoRng + ?Sized>(
244        &self,
245        rng: &mut R,
246        prehash: &[u8],
247    ) -> Result<(Signature<C>, RecoveryId)> {
248        self.sign_prehash_recoverable_with_rng(rng, prehash)
249            .map_err(|_| Error::new())
250    }
251}
252
253#[cfg(feature = "algorithm")]
254impl<C, D> RandomizedDigestSigner<D, (Signature<C>, RecoveryId)> for SigningKey<C>
255where
256    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
257    D: Digest + Update,
258    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
259{
260    fn try_sign_digest_with_rng<R: TryCryptoRng + ?Sized, F: Fn(&mut D) -> Result<()>>(
261        &self,
262        rng: &mut R,
263        f: F,
264    ) -> Result<(Signature<C>, RecoveryId)> {
265        let mut digest = D::new();
266        f(&mut digest)?;
267        self.sign_prehash_with_rng(rng, &digest.finalize())
268    }
269}
270
271#[cfg(feature = "algorithm")]
272impl<C> PrehashSigner<(Signature<C>, RecoveryId)> for SigningKey<C>
273where
274    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
275    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
276{
277    fn sign_prehash(&self, prehash: &[u8]) -> Result<(Signature<C>, RecoveryId)> {
278        Ok(self.sign_prehash_recoverable(prehash))
279    }
280}
281
282#[cfg(feature = "algorithm")]
283impl<C> Signer<(Signature<C>, RecoveryId)> for SigningKey<C>
284where
285    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
286    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
287{
288    fn try_sign(&self, msg: &[u8]) -> Result<(Signature<C>, RecoveryId)> {
289        self.try_multipart_sign(&[msg])
290    }
291}
292
293#[cfg(feature = "algorithm")]
294impl<C> MultipartSigner<(Signature<C>, RecoveryId)> for SigningKey<C>
295where
296    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
297    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
298{
299    fn try_multipart_sign(&self, msg: &[&[u8]]) -> Result<(Signature<C>, RecoveryId)> {
300        let mut digest = C::Digest::new();
301        msg.iter()
302            .for_each(|slice| Update::update(&mut digest, slice));
303        Ok(self.sign_digest_recoverable(digest))
304    }
305}
306
307#[cfg(feature = "algorithm")]
308impl<C> VerifyingKey<C>
309where
310    C: EcdsaCurve + CurveArithmetic,
311    AffinePoint<C>: DecompressPoint<C> + FromSec1Point<C> + ToSec1Point<C>,
312    FieldBytesSize<C>: sec1::ModulusSize,
313{
314    /// Recover a [`VerifyingKey`] from the given message, signature, and [`RecoveryId`].
315    ///
316    /// The message is first hashed using this curve's [`DigestAlgorithm`].
317    ///
318    /// # Errors
319    /// Returns [`Error`] if the recovered elliptic curve point is the additive identity.
320    pub fn recover_from_msg(
321        msg: &[u8],
322        signature: &Signature<C>,
323        recovery_id: RecoveryId,
324    ) -> Result<Self>
325    where
326        C: DigestAlgorithm,
327    {
328        Self::recover_from_digest(C::Digest::new_with_prefix(msg), signature, recovery_id)
329    }
330
331    /// Recover a [`VerifyingKey`] from the given message [`Digest`], signature, and [`RecoveryId`].
332    ///
333    /// # Errors
334    /// Returns [`Error`] if the recovered elliptic curve point is the additive identity.
335    pub fn recover_from_digest<D>(
336        msg_digest: D,
337        signature: &Signature<C>,
338        recovery_id: RecoveryId,
339    ) -> Result<Self>
340    where
341        D: Digest,
342    {
343        Self::recover_from_prehash(&msg_digest.finalize(), signature, recovery_id)
344    }
345
346    /// Recover a [`VerifyingKey`] from the given `prehash` of a message, the signature over that
347    /// prehashed message, and a [`RecoveryId`].
348    ///
349    /// <div class="warning">
350    /// <b>Security Warning</b>
351    ///
352    /// The `prehash` argument must be the output of a secure digest function, e.g. Keccak256
353    /// or SHA-256.
354    ///
355    /// Failure to use such a digest algorithm to compute `prehash` allows an attacker to solve for
356    /// it in a system of linear equations that can cause the recovery function to output any public
357    /// key the attacker wants.
358    /// </div>
359    ///
360    /// # Errors
361    /// Returns [`Error`] if the recovered elliptic curve point is the additive identity.
362    #[allow(non_snake_case)]
363    pub fn recover_from_prehash(
364        prehash: &[u8],
365        signature: &Signature<C>,
366        recovery_id: RecoveryId,
367    ) -> Result<Self> {
368        let (r, s) = signature.split_scalars();
369        let z = bytes2scalar::<C>(prehash);
370
371        let r_bytes = if recovery_id.is_x_reduced() {
372            let uint = field::bytes_to_uint::<C>(&r.to_repr())
373                .checked_add(&C::ORDER)
374                .into_option()
375                .ok_or_else(Error::new)?;
376
377            field::uint_to_bytes::<C>(&uint)
378        } else {
379            r.to_repr()
380        };
381
382        let R: ProjectivePoint<C> =
383            AffinePoint::<C>::decompress(&r_bytes, u8::from(recovery_id.is_y_odd()).into())
384                .into_option()
385                .ok_or_else(Error::new)?
386                .into();
387
388        let r_inv = *r.invert();
389        let u1 = -(r_inv * z);
390        let u2 = r_inv * *s;
391        let pk = ProjectivePoint::<C>::mul_by_generator_and_mul_add_vartime(&u1, &u2, &R);
392        Self::from_affine(pk.to_affine())
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::RecoveryId;
399
400    #[test]
401    fn new() {
402        assert_eq!(RecoveryId::new(false, false).to_byte(), 0);
403        assert_eq!(RecoveryId::new(true, false).to_byte(), 1);
404        assert_eq!(RecoveryId::new(false, true).to_byte(), 2);
405        assert_eq!(RecoveryId::new(true, true).to_byte(), 3);
406    }
407
408    #[test]
409    fn try_from() {
410        for n in 0u8..=3 {
411            assert_eq!(RecoveryId::try_from(n).expect("RecoveryId").to_byte(), n);
412        }
413
414        for n in 4u8..=255 {
415            assert!(RecoveryId::try_from(n).is_err());
416        }
417    }
418
419    #[test]
420    fn is_x_reduced() {
421        assert!(!RecoveryId::try_from(0).expect("RecoveryId").is_x_reduced());
422        assert!(!RecoveryId::try_from(1).expect("RecoveryId").is_x_reduced());
423        assert!(RecoveryId::try_from(2).expect("RecoveryId").is_x_reduced());
424        assert!(RecoveryId::try_from(3).expect("RecoveryId").is_x_reduced());
425    }
426
427    #[test]
428    fn is_y_odd() {
429        assert!(!RecoveryId::try_from(0).expect("RecoveryId").is_y_odd());
430        assert!(RecoveryId::try_from(1).expect("RecoveryId").is_y_odd());
431        assert!(!RecoveryId::try_from(2).expect("RecoveryId").is_y_odd());
432        assert!(RecoveryId::try_from(3).expect("RecoveryId").is_y_odd());
433    }
434}