Coverage Report

Created: 2026-11-12 06:16

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/ecdsa-0.17.0/src/recovery.rs
Line
Count
Source
1
//! Public key recovery support.
2
3
use crate::{Error, Result};
4
5
#[cfg(feature = "algorithm")]
6
use {
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)]
41
pub struct RecoveryId(pub(crate) u8);
42
43
impl 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
2.16k
    pub const fn new(is_y_odd: bool, is_x_reduced: bool) -> Self {
54
2.16k
        Self(((is_x_reduced as u8) << 1) | (is_y_odd as u8))
55
2.16k
    }
56
57
    /// Did the affine x-coordinate of 𝑘×𝑮 overflow the curve order?
58
    #[must_use]
59
0
    pub const fn is_x_reduced(self) -> bool {
60
0
        (self.0 & 0b10) != 0
61
0
    }
62
63
    /// Is the affine y-coordinate of 𝑘×𝑮 odd?
64
    #[must_use]
65
0
    pub const fn is_y_odd(self) -> bool {
66
0
        (self.0 & 1) != 0
67
0
    }
68
69
    /// Convert a `u8` into a [`RecoveryId`].
70
    #[must_use]
71
0
    pub const fn from_byte(byte: u8) -> Option<Self> {
72
0
        if byte <= Self::MAX {
73
0
            Some(Self(byte))
74
        } else {
75
0
            None
76
        }
77
0
    }
78
79
    /// Convert this [`RecoveryId`] into a `u8`.
80
    #[must_use]
81
0
    pub const fn to_byte(self) -> u8 {
82
0
        self.0
83
0
    }
84
}
85
86
#[cfg(feature = "algorithm")]
87
impl 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
0
    pub fn trial_recovery_from_msg<C>(
95
0
        verifying_key: &VerifyingKey<C>,
96
0
        msg: &[u8],
97
0
        signature: &Signature<C>,
98
0
    ) -> Result<Self>
99
0
    where
100
0
        C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
101
0
        AffinePoint<C>: DecompressPoint<C> + FromSec1Point<C> + ToSec1Point<C>,
102
0
        FieldBytesSize<C>: sec1::ModulusSize,
103
    {
104
0
        Self::trial_recovery_from_digest(verifying_key, C::Digest::new_with_prefix(msg), signature)
105
0
    }
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
0
    pub fn trial_recovery_from_digest<C, D>(
114
0
        verifying_key: &VerifyingKey<C>,
115
0
        digest: D,
116
0
        signature: &Signature<C>,
117
0
    ) -> Result<Self>
118
0
    where
119
0
        C: EcdsaCurve + CurveArithmetic,
120
0
        D: Digest,
121
0
        AffinePoint<C>: DecompressPoint<C> + FromSec1Point<C> + ToSec1Point<C>,
122
0
        FieldBytesSize<C>: sec1::ModulusSize,
123
    {
124
0
        Self::trial_recovery_from_prehash(verifying_key, &digest.finalize(), signature)
125
0
    }
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
0
    pub fn trial_recovery_from_prehash<C>(
134
0
        verifying_key: &VerifyingKey<C>,
135
0
        prehash: &[u8],
136
0
        signature: &Signature<C>,
137
0
    ) -> Result<Self>
138
0
    where
139
0
        C: EcdsaCurve + CurveArithmetic,
140
0
        AffinePoint<C>: DecompressPoint<C> + FromSec1Point<C> + ToSec1Point<C>,
141
0
        FieldBytesSize<C>: sec1::ModulusSize,
142
    {
143
        // Ensure signature verifies with the provided key
144
0
        verify_prehashed::<C>(
145
0
            &ProjectivePoint::<C>::from(*verifying_key.as_affine()),
146
0
            prehash,
147
0
            signature,
148
0
        )?;
149
150
0
        for id in 0..=Self::MAX {
151
0
            let recovery_id = RecoveryId(id);
152
153
0
            if let Ok(vk) = VerifyingKey::recover_from_prehash(prehash, signature, recovery_id) {
154
0
                if verifying_key == &vk {
155
0
                    return Ok(recovery_id);
156
0
                }
157
0
            }
158
        }
159
160
0
        Err(Error::new())
161
0
    }
162
}
163
164
impl TryFrom<u8> for RecoveryId {
165
    type Error = Error;
166
167
0
    fn try_from(byte: u8) -> Result<Self> {
168
0
        Self::from_byte(byte).ok_or_else(Error::new)
169
0
    }
170
}
171
172
impl From<RecoveryId> for u8 {
173
0
    fn from(id: RecoveryId) -> u8 {
174
0
        id.0
175
0
    }
176
}
177
178
#[cfg(feature = "algorithm")]
179
impl<C> SigningKey<C>
180
where
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
0
    pub fn sign_prehash_recoverable_with_rng<R: TryCryptoRng + ?Sized>(
190
0
        &self,
191
0
        rng: &mut R,
192
0
        prehash: &[u8],
193
0
    ) -> core::result::Result<(Signature<C>, RecoveryId), R::Error> {
194
0
        let mut ad = FieldBytes::<C>::default();
195
0
        rng.try_fill_bytes(&mut ad)?;
196
0
        Ok(sign_prehashed_rfc6979::<C, C::Digest>(
197
0
            self.as_nonzero_scalar(),
198
0
            prehash,
199
0
            &ad,
200
0
        ))
201
0
    }
202
203
    /// Sign the given message prehash, returning a signature and recovery ID.
204
0
    pub fn sign_prehash_recoverable(&self, prehash: &[u8]) -> (Signature<C>, RecoveryId) {
205
0
        sign_prehashed_rfc6979::<C, C::Digest>(self.as_nonzero_scalar(), prehash, b"")
206
0
    }
207
208
    /// Sign the given message digest, returning a signature and recovery ID.
209
0
    pub fn sign_digest_recoverable<D: Digest>(&self, msg_digest: D) -> (Signature<C>, RecoveryId) {
210
0
        self.sign_prehash_recoverable(&msg_digest.finalize())
211
0
    }
212
213
    /// Sign the given message, hashing it with the curve's default digest
214
    /// function, and returning a signature and recovery ID.
215
0
    pub fn sign_recoverable(&self, msg: &[u8]) -> (Signature<C>, RecoveryId) {
216
0
        self.sign_digest_recoverable(C::Digest::new_with_prefix(msg))
217
0
    }
218
}
219
220
#[cfg(feature = "algorithm")]
221
impl<C, D> DigestSigner<D, (Signature<C>, RecoveryId)> for SigningKey<C>
222
where
223
    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
224
    D: Digest + Update,
225
    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
226
{
227
0
    fn try_sign_digest<F: Fn(&mut D) -> Result<()>>(
228
0
        &self,
229
0
        f: F,
230
0
    ) -> Result<(Signature<C>, RecoveryId)> {
231
0
        let mut digest = D::new();
232
0
        f(&mut digest)?;
233
0
        Ok(self.sign_digest_recoverable(digest))
234
0
    }
235
}
236
237
#[cfg(feature = "algorithm")]
238
impl<C> RandomizedPrehashSigner<(Signature<C>, RecoveryId)> for SigningKey<C>
239
where
240
    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
241
    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
242
{
243
0
    fn sign_prehash_with_rng<R: TryCryptoRng + ?Sized>(
244
0
        &self,
245
0
        rng: &mut R,
246
0
        prehash: &[u8],
247
0
    ) -> Result<(Signature<C>, RecoveryId)> {
248
0
        self.sign_prehash_recoverable_with_rng(rng, prehash)
249
0
            .map_err(|_| Error::new())
250
0
    }
251
}
252
253
#[cfg(feature = "algorithm")]
254
impl<C, D> RandomizedDigestSigner<D, (Signature<C>, RecoveryId)> for SigningKey<C>
255
where
256
    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
257
    D: Digest + Update,
258
    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
259
{
260
0
    fn try_sign_digest_with_rng<R: TryCryptoRng + ?Sized, F: Fn(&mut D) -> Result<()>>(
261
0
        &self,
262
0
        rng: &mut R,
263
0
        f: F,
264
0
    ) -> Result<(Signature<C>, RecoveryId)> {
265
0
        let mut digest = D::new();
266
0
        f(&mut digest)?;
267
0
        self.sign_prehash_with_rng(rng, &digest.finalize())
268
0
    }
269
}
270
271
#[cfg(feature = "algorithm")]
272
impl<C> PrehashSigner<(Signature<C>, RecoveryId)> for SigningKey<C>
273
where
274
    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
275
    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
276
{
277
0
    fn sign_prehash(&self, prehash: &[u8]) -> Result<(Signature<C>, RecoveryId)> {
278
0
        Ok(self.sign_prehash_recoverable(prehash))
279
0
    }
280
}
281
282
#[cfg(feature = "algorithm")]
283
impl<C> Signer<(Signature<C>, RecoveryId)> for SigningKey<C>
284
where
285
    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
286
    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
287
{
288
0
    fn try_sign(&self, msg: &[u8]) -> Result<(Signature<C>, RecoveryId)> {
289
0
        self.try_multipart_sign(&[msg])
290
0
    }
291
}
292
293
#[cfg(feature = "algorithm")]
294
impl<C> MultipartSigner<(Signature<C>, RecoveryId)> for SigningKey<C>
295
where
296
    C: EcdsaCurve + CurveArithmetic + DigestAlgorithm,
297
    Scalar<C>: Invert<Output = CtOption<Scalar<C>>>,
298
{
299
0
    fn try_multipart_sign(&self, msg: &[&[u8]]) -> Result<(Signature<C>, RecoveryId)> {
300
0
        let mut digest = C::Digest::new();
301
0
        msg.iter()
302
0
            .for_each(|slice| Update::update(&mut digest, slice));
303
0
        Ok(self.sign_digest_recoverable(digest))
304
0
    }
305
}
306
307
#[cfg(feature = "algorithm")]
308
impl<C> VerifyingKey<C>
309
where
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
0
    pub fn recover_from_msg(
321
0
        msg: &[u8],
322
0
        signature: &Signature<C>,
323
0
        recovery_id: RecoveryId,
324
0
    ) -> Result<Self>
325
0
    where
326
0
        C: DigestAlgorithm,
327
    {
328
0
        Self::recover_from_digest(C::Digest::new_with_prefix(msg), signature, recovery_id)
329
0
    }
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
0
    pub fn recover_from_digest<D>(
336
0
        msg_digest: D,
337
0
        signature: &Signature<C>,
338
0
        recovery_id: RecoveryId,
339
0
    ) -> Result<Self>
340
0
    where
341
0
        D: Digest,
342
    {
343
0
        Self::recover_from_prehash(&msg_digest.finalize(), signature, recovery_id)
344
0
    }
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
0
    pub fn recover_from_prehash(
364
0
        prehash: &[u8],
365
0
        signature: &Signature<C>,
366
0
        recovery_id: RecoveryId,
367
0
    ) -> Result<Self> {
368
0
        let (r, s) = signature.split_scalars();
369
0
        let z = bytes2scalar::<C>(prehash);
370
371
0
        let r_bytes = if recovery_id.is_x_reduced() {
372
0
            let uint = field::bytes_to_uint::<C>(&r.to_repr())
373
0
                .checked_add(&C::ORDER)
374
0
                .into_option()
375
0
                .ok_or_else(Error::new)?;
376
377
0
            field::uint_to_bytes::<C>(&uint)
378
        } else {
379
0
            r.to_repr()
380
        };
381
382
0
        let R: ProjectivePoint<C> =
383
0
            AffinePoint::<C>::decompress(&r_bytes, u8::from(recovery_id.is_y_odd()).into())
384
0
                .into_option()
385
0
                .ok_or_else(Error::new)?
386
0
                .into();
387
388
0
        let r_inv = *r.invert();
389
0
        let u1 = -(r_inv * z);
390
0
        let u2 = r_inv * *s;
391
0
        let pk = ProjectivePoint::<C>::mul_by_generator_and_mul_add_vartime(&u1, &u2, &R);
392
0
        Self::from_affine(pk.to_affine())
393
0
    }
394
}
395
396
#[cfg(test)]
397
mod 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
}