Skip to content

Commit e5ff381

Browse files
fix(k256): ecdsa verify needs custom hook (#1744)
For some reason, unlike other libraries, the k256 crate has a special requirement that the signature is normalized ahead of time: https://github.com/RustCrypto/elliptic-curves/blob/5ac8f5d77f11399ff48d87b0554935f6eddda342/k256/src/ecdsa.rs#L203 To handle this, we introduce a new trait `VerifyCustomHook` that is called both in `recover_prehashed` and `verify_prehash`. - [x] Provide a proof that valid pubkey recovery implies the ECDSA signature verification algorithm will automatically pass, so there is no need for `recover_prehashed` to call `verify_prehash` again.
1 parent 7b4b59f commit e5ff381

File tree

11 files changed

+253
-43
lines changed

11 files changed

+253
-43
lines changed

extensions/ecc/guest/ECDSA.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# ECDSA Signature Verification and Recovery
2+
3+
We give a proof that the ECDSA public key recovery algorithm used in the `ecdsa` module automatically implies valid signature verification.
4+
5+
## `verify_prehashed`
6+
We start by giving an overview of the ECDSA signature verification algorithm following the [Digital Signature Standard](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-5.pdf).
7+
8+
We describe the algorithm for `verify_prehashed`, which does not include the hashing of the signature itself.
9+
10+
**Inputs:**
11+
1. Digest $H$ which _should_ be the digest of a cryptographic hash function on the message.
12+
2. Signature consisting of pair of integers $r, s$.
13+
3. Purported signature verification key $Q$.
14+
15+
The `verify_prehashed` function assumes `sig: &[u8]` is properly encoded, but the `PrehashVerifier::verify_prehash` function takes in `signature: &Signature<C>` which is a protected type ensuring correct encoding. The protected type `VerifyingKey<C>` and its inner type `PublicKey<C>` are protected to ensure $Q$ is a non-identity affine point on the elliptic curve.
16+
17+
**Output:** Accept or reject the signature over $H$ as originating from the owner of public key $Q$.
18+
19+
**Process:**
20+
Let $n$ be the order of the elliptic curve $C$ and $L_n$ the number of bits in $n$. We assume $n$ is of prime order. Let $G$ be a fixed generator point of $C$. Let $p$ be the modulus of the coordinate field of $C$.
21+
22+
1. Verify that $r$ and $s$ are integers in the interval $[1, n-1]$. Reject otherwise.
23+
2. Derive the integer $e$ from $H$ as follows:
24+
- If the bit length of $H$ is at most $L_n$, set $E = H$. Otherwise set $E$ equal to the leftmost $L_n$ bits of $H$.
25+
- Let $e$ denote the integer representation of $E$ in big endian.
26+
3. Compute $u = e \cdot s^{-1} \mod n$ and $v = r \cdot s^{-1} \mod n$.
27+
4. Compute $R_1 = uG + vQ$. Reject if $R_1$ is the identity.
28+
5. Set $x_R$ to the $x$-coordinate of $R_1 = (x_R, y_R)$.
29+
6. Convert $x_R$ to the unique integer $r_1$ in $[0, p - 1]$.
30+
7. Accept if and only if $r = r_1 \mod n$.
31+
32+
## `recover_from_prehash_noverify`
33+
34+
We describe the ECDSA public key recovery algorithm following [SEC 1, Section 4.1.6](https://www.secg.org/sec1-v2.pdf). Like above, we describe this algorithm for `recover_from_prehash_noverify` which operates on the message digest without hashing the message.
35+
36+
**Inputs:**
37+
1. Digest $H$ which _should_ be the digest of a cryptographic hash function on the message.
38+
2. Signature consisting of pair of integers $r, s$.
39+
3. Recovery ID `recovery_id` in the range $[0, 3]$.
40+
41+
**Output:** An elliptic curve public key $Q$ for which $(r, s)$ is a valid signature on digest $H$, or "invalid".
42+
43+
**Process:**
44+
Let $n, L_n, G, p, C$ be as in the previous section.
45+
46+
1. Verify that $r$ and $s$ are integers in the interval $[1, n-1]$. Reject otherwise.
47+
2. Let $j = 0$ if the high bit (3/4) of `recovery_id` is $0$, otherwise $j = 1$. Note that the current `RecoveryId` from the `ecdsa` crate only supports $j$ in $[0, 1]$.
48+
3. Let $x = r + j n$ as integer. Reject if $x \geq p$.
49+
4. Calculate the curve point $R = (x_1, y_1)$ where $x_1 = x \mod p$. Reject if no such $y_1$ exists. If more than one $y_1$ exists, then choose the unique $y_1$ such that the parity of $y_1$ as an integer in canonical form matches the low bit of `recovery_id`.
50+
5. Compute $e$ from $H$ as in Step 2 of ECDSA signature verification above.
51+
6. Compute $Q = r^{-1}( s R - e G)$.
52+
7. Reject if $Q$ is the identity. Accept otherwise.
53+
54+
### Proof of signature verification
55+
56+
Above, we skip the verification of the original signature again with the recovered $Q$. We give a proof below that the recovered $Q$ will always pass the signature verification.
57+
58+
We refer to the steps in verification as V1-7 and the steps in recovery as R1-7.
59+
- V1 is automatic from R1.
60+
- V2 is the same as R5.
61+
- V3: We compute $u = e \cdot s^{-1} \mod n$ and $v = r \cdot s^{-1} \mod n$.
62+
- V4: compute $$R_1 = uG + vQ = e \cdot s^{-1} G + (r \cdot s^{-1})\cdot r^{-1} \cdot (s R - e G) = R.$$
63+
R4 guarantees $R_1$ is not identity.
64+
- V5: We set $x_R = x_1$.
65+
- V6: Then $r_1 = r + j n$ since R3 guarantees that $r + j n$ is in the range $[0, p-1]$.
66+
- V7: It is evident that $r_1 = r+jn$ is congruent to $r$ modulo $n$.
67+
68+
Therefore the signature verifies with respect to $Q$.

extensions/ecc/guest/src/ecdsa.rs

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,7 @@ where
218218
impl<C> VerifyingKey<C>
219219
where
220220
C: IntrinsicCurve + PrimeCurve,
221-
C::Point: WeierstrassPoint + CyclicGroup + FromCompressed<Coordinate<C>>,
221+
C::Point: WeierstrassPoint + CyclicGroup + FromCompressed<Coordinate<C>> + VerifyCustomHook<C>,
222222
Coordinate<C>: IntMod,
223223
C::Scalar: IntMod + Reduce,
224224
for<'a> &'a C::Point: Add<&'a C::Point, Output = C::Point>,
@@ -263,7 +263,34 @@ where
263263
recovery_id: RecoveryId,
264264
) -> Result<Self> {
265265
let sig = signature.to_bytes();
266-
Self::recover_from_prehash_noverify(prehash, &sig, recovery_id)
266+
let vk = Self::recover_from_prehash_noverify(prehash, &sig, recovery_id)?;
267+
vk.inner.as_affine().verify_hook(prehash, signature)?;
268+
Ok(vk)
269+
}
270+
}
271+
272+
/// To match the RustCrypto trait [VerifyPrimitive]. Certain curves have special verification logic
273+
/// outside of the general ECDSA verification algorithm. This trait provides a hook for such logic.
274+
///
275+
/// This trait is intended to be implemented on type which can access
276+
/// the affine point representing the public key via `&self`, such as a
277+
/// particular curve's `AffinePoint` type.
278+
pub trait VerifyCustomHook<C>: WeierstrassPoint
279+
where
280+
C: IntrinsicCurve + PrimeCurve,
281+
SignatureSize<C>: ArrayLength<u8>,
282+
{
283+
/// This is **NOT** the full ECDSA signature verification algorithm. The implementer should only
284+
/// add additional verification logic not contained in [verify_prehashed]. The default
285+
/// implementation does nothing.
286+
///
287+
/// Accepts the following arguments:
288+
///
289+
/// - `z`: message digest to be verified. MUST BE OUTPUT OF A CRYPTOGRAPHICALLY SECURE DIGEST
290+
/// ALGORITHM!!!
291+
/// - `sig`: signature to be verified against the key and message
292+
fn verify_hook(&self, _z: &[u8], _sig: &Signature<C>) -> Result<()> {
293+
Ok(())
267294
}
268295
}
269296

@@ -276,17 +303,17 @@ where
276303
C: PrimeCurve + IntrinsicCurve,
277304
D: Digest + FixedOutput<OutputSize = FieldBytesSize<C>>,
278305
SignatureSize<C>: ArrayLength<u8>,
279-
C::Point: WeierstrassPoint + CyclicGroup + FromCompressed<Coordinate<C>>,
306+
C::Point: WeierstrassPoint + CyclicGroup + FromCompressed<Coordinate<C>> + VerifyCustomHook<C>,
280307
Coordinate<C>: IntMod,
281308
<C as IntrinsicCurve>::Scalar: IntMod + Reduce,
282309
for<'a> &'a C::Point: Add<&'a C::Point, Output = C::Point>,
283310
for<'a> &'a Scalar<C>: DivUnsafe<&'a Scalar<C>, Output = Scalar<C>>,
284311
{
285312
fn verify_digest(&self, msg_digest: D, signature: &Signature<C>) -> Result<()> {
286-
verify_prehashed::<C>(
287-
self.inner.as_affine().clone(),
313+
PrehashVerifier::<Signature<C>>::verify_prehash(
314+
self,
288315
&msg_digest.finalize_fixed(),
289-
&signature.to_bytes(),
316+
signature,
290317
)
291318
}
292319
}
@@ -295,13 +322,14 @@ impl<C> PrehashVerifier<Signature<C>> for VerifyingKey<C>
295322
where
296323
C: PrimeCurve + IntrinsicCurve,
297324
SignatureSize<C>: ArrayLength<u8>,
298-
C::Point: WeierstrassPoint + CyclicGroup + FromCompressed<Coordinate<C>>,
325+
C::Point: WeierstrassPoint + CyclicGroup + FromCompressed<Coordinate<C>> + VerifyCustomHook<C>,
299326
Coordinate<C>: IntMod,
300327
C::Scalar: IntMod + Reduce,
301328
for<'a> &'a C::Point: Add<&'a C::Point, Output = C::Point>,
302329
for<'a> &'a Scalar<C>: DivUnsafe<&'a Scalar<C>, Output = Scalar<C>>,
303330
{
304331
fn verify_prehash(&self, prehash: &[u8], signature: &Signature<C>) -> Result<()> {
332+
self.inner.as_affine().verify_hook(prehash, signature)?;
305333
verify_prehashed::<C>(
306334
self.inner.as_affine().clone(),
307335
prehash,
@@ -314,7 +342,7 @@ impl<C> Verifier<Signature<C>> for VerifyingKey<C>
314342
where
315343
C: PrimeCurve + CurveArithmetic + DigestPrimitive + IntrinsicCurve,
316344
SignatureSize<C>: ArrayLength<u8>,
317-
C::Point: WeierstrassPoint + CyclicGroup + FromCompressed<Coordinate<C>>,
345+
C::Point: WeierstrassPoint + CyclicGroup + FromCompressed<Coordinate<C>> + VerifyCustomHook<C>,
318346
Coordinate<C>: IntMod,
319347
<C as IntrinsicCurve>::Scalar: IntMod + Reduce,
320348
for<'a> &'a C::Point: Add<&'a C::Point, Output = C::Point>,
@@ -454,6 +482,9 @@ where
454482
}
455483
assert!(FieldBytesSize::<C>::USIZE <= Coordinate::<C>::NUM_LIMBS);
456484
let x = Coordinate::<C>::from_be_bytes(&r_bytes);
485+
if !x.is_reduced() {
486+
return Err(Error::new());
487+
}
457488
let rec_id = recovery_id.to_byte();
458489
// The point R decompressed from x-coordinate `r`
459490
let R: C::Point = FromCompressed::decompress(x, &rec_id).ok_or_else(Error::new)?;
@@ -468,6 +499,7 @@ where
468499
}
469500
}
470501

502+
/// Assumes that `sig` is proper encoding of `r, s`.
471503
// Ref: https://docs.rs/ecdsa/latest/src/ecdsa/hazmat.rs.html#270
472504
#[allow(non_snake_case)]
473505
pub fn verify_prehashed<C>(pubkey: AffinePoint<C>, prehash: &[u8], sig: &[u8]) -> Result<()>
@@ -510,6 +542,8 @@ where
510542
// public key
511543
let Q = pubkey;
512544
let R = <C as IntrinsicCurve>::msm(&[u1, u2], &[G, Q]);
545+
// For Coordinate<C>: IntMod, the internal implementation of is_identity will assert x, y
546+
// coordinates of R are both reduced.
513547
if R.is_identity() {
514548
return Err(Error::new());
515549
}

extensions/ecc/tests/programs/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,9 +70,13 @@ name = "ecdsa"
7070
required-features = ["k256"]
7171

7272
[[example]]
73-
name = "ecdsa_recover"
73+
name = "ecdsa_recover_p256"
7474
required-features = ["p256"]
7575

76+
[[example]]
77+
name = "ecdsa_recover_k256"
78+
required-features = ["k256"]
79+
7680
[[example]]
7781
name = "invalid_setup"
7882
required-features = ["k256", "p256"]
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
#![cfg_attr(not(feature = "std"), no_main)]
2+
#![cfg_attr(not(feature = "std"), no_std)]
3+
4+
extern crate alloc;
5+
6+
use alloc::vec::Vec;
7+
8+
use ecdsa_core::{signature::hazmat::PrehashVerifier, RecoveryId};
9+
use openvm::io::read;
10+
use openvm_ecc_test_programs::RecoveryTestVector;
11+
#[allow(unused_imports)]
12+
use openvm_k256::{
13+
ecdsa::{Signature, VerifyingKey},
14+
Secp256k1Coord, Secp256k1Point,
15+
};
16+
17+
openvm::entry!(main);
18+
19+
openvm::init!("openvm_init_ec_k256.rs");
20+
21+
pub fn main() {
22+
let test_vectors: Vec<RecoveryTestVector> = read();
23+
for vector in test_vectors {
24+
let sig = match Signature::try_from(vector.sig.as_slice()) {
25+
Ok(_v) => _v,
26+
Err(_) => {
27+
assert_eq!(vector.ok, false);
28+
continue;
29+
}
30+
};
31+
let recid = match RecoveryId::from_byte(vector.recid) {
32+
Some(_v) => _v,
33+
None => {
34+
assert_eq!(vector.ok, false);
35+
continue;
36+
}
37+
};
38+
let vk = match VerifyingKey::recover_from_prehash(&vector.msg, &sig, recid) {
39+
Ok(_v) => _v,
40+
Err(_) => {
41+
openvm::io::println("Recovery failed");
42+
assert_eq!(vector.ok, false);
43+
continue;
44+
}
45+
};
46+
openvm::io::println(alloc::format!("{:?}", vk.to_sec1_bytes(false)));
47+
vk.verify_prehash(&vector.msg, &sig).unwrap();
48+
// If reached here, recovery succeeded
49+
assert_eq!(vector.ok, true);
50+
}
51+
}

extensions/ecc/tests/programs/examples/ecdsa_recover.rs renamed to extensions/ecc/tests/programs/examples/ecdsa_recover_p256.rs

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,16 @@ use alloc::vec::Vec;
77

88
use ecdsa_core::RecoveryId;
99
use openvm::io::read;
10+
use openvm_ecc_test_programs::RecoveryTestVector;
1011
#[allow(unused_imports)]
1112
use openvm_p256::{
1213
ecdsa::{Signature, VerifyingKey},
1314
P256Coord, P256Point,
1415
};
15-
use serde::{Deserialize, Serialize};
16-
use serde_with::{serde_as, Bytes};
1716

1817
openvm::entry!(main);
1918

20-
openvm::init!("openvm_init_ecdsa_recover_p256.rs");
21-
22-
/// Signature recovery test vectors
23-
#[repr(C)]
24-
#[serde_as]
25-
#[derive(Serialize, Deserialize)]
26-
struct RecoveryTestVector {
27-
#[serde_as(as = "Bytes")]
28-
pk: [u8; 33],
29-
#[serde_as(as = "Bytes")]
30-
msg: [u8; 32],
31-
#[serde_as(as = "Bytes")]
32-
sig: [u8; 64],
33-
recid: u8,
34-
ok: bool,
35-
}
19+
openvm::init!("openvm_init_ec_nonzero_a_p256.rs");
3620

3721
pub fn main() {
3822
let test_vectors: Vec<RecoveryTestVector> = read();
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[app_vm_config.rv32i]
2+
[app_vm_config.rv32m]
3+
[app_vm_config.io]
4+
5+
[app_vm_config.modular]
6+
supported_moduli = [
7+
"115792089237316195423570985008687907853269984665640564039457584007908834671663",
8+
"115792089237316195423570985008687907852837564279074904382605163141518161494337",
9+
]
10+
11+
[[app_vm_config.ecc.supported_curves]]
12+
struct_name = "Secp256k1Point"
13+
modulus = "115792089237316195423570985008687907853269984665640564039457584007908834671663"
14+
scalar = "115792089237316195423570985008687907852837564279074904382605163141518161494337"
15+
a = "0"
16+
b = "7"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#![no_std]
2+
3+
use serde::{Deserialize, Serialize};
4+
use serde_with::{serde_as, Bytes};
5+
6+
/// Signature recovery test vectors
7+
#[repr(C)]
8+
#[serde_as]
9+
#[derive(Serialize, Deserialize)]
10+
pub struct RecoveryTestVector {
11+
#[serde_as(as = "Bytes")]
12+
pub pk: [u8; 33],
13+
#[serde_as(as = "Bytes")]
14+
pub msg: [u8; 32],
15+
#[serde_as(as = "Bytes")]
16+
pub sig: [u8; 64],
17+
pub recid: u8,
18+
pub ok: bool,
19+
}

extensions/ecc/tests/src/lib.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ mod tests {
2828
};
2929
use openvm_transpiler::{transpiler::Transpiler, FromElf};
3030

31-
use crate::test_vectors::P256_RECOVERY_TEST_VECTORS;
31+
use crate::test_vectors::{K256_RECOVERY_TEST_VECTORS, P256_RECOVERY_TEST_VECTORS};
32+
3233
type F = BabyBear;
3334

3435
#[test]
@@ -188,9 +189,9 @@ mod tests {
188189
.app_vm_config;
189190
let elf = build_example_program_at_path_with_features(
190191
get_programs_dir!(),
191-
"ecdsa_recover",
192+
"ecdsa_recover_p256",
192193
["p256"],
193-
&config,
194+
&NoInitFile, // using already created file
194195
)?;
195196
let openvm_exe = VmExe::from_elf(elf, config.transpiler())?;
196197
let mut input = StdIn::default();
@@ -199,6 +200,24 @@ mod tests {
199200
Ok(())
200201
}
201202

203+
#[test]
204+
fn test_k256_ecdsa_recover() -> Result<()> {
205+
let config =
206+
toml::from_str::<AppConfig<SdkVmConfig>>(include_str!("../programs/openvm_k256.toml"))?
207+
.app_vm_config;
208+
let elf = build_example_program_at_path_with_features(
209+
get_programs_dir!(),
210+
"ecdsa_recover_k256",
211+
["k256"],
212+
&NoInitFile, // using already created file
213+
)?;
214+
let openvm_exe = VmExe::from_elf(elf, config.transpiler())?;
215+
let mut input = StdIn::default();
216+
input.write(&K256_RECOVERY_TEST_VECTORS.to_vec());
217+
air_test_with_min_segments(config, openvm_exe, input, 1);
218+
Ok(())
219+
}
220+
202221
#[test]
203222
#[should_panic]
204223
fn test_invalid_setup() {

extensions/ecc/tests/src/test_vectors.rs

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,11 @@ pub struct RecoveryTestVector {
1717
}
1818

1919
#[allow(dead_code)]
20-
pub const P256_RECOVERY_TEST_VECTORS: &[RecoveryTestVector] = &[RecoveryTestVector {
21-
pk: hex!("020000000000000000000000000000000000000000000000000000000000000000"),
22-
msg: hex!("00000000000000000000FFFFFFFF03030BFFFFFFFFFF030BFFFFFFFFFFFFF8FC"),
23-
sig: hex!("00000000ffffffff00000000000000004319055258e8617b0c46353d039cdaaf0000000000000000000000000000000000000000000000000000000000000001"),
24-
recid: 2,
25-
ok: false,
20+
pub const P256_RECOVERY_TEST_VECTORS: &[RecoveryTestVector] = &[RecoveryTestVector {pk:hex!("020000000000000000000000000000000000000000000000000000000000000000"),msg:hex!("00000000000000000000FFFFFFFF03030BFFFFFFFFFF030BFFFFFFFFFFFFF8FC"),sig:hex!("00000000ffffffff00000000000000004319055258e8617b0c46353d039cdaaf0000000000000000000000000000000000000000000000000000000000000001"),recid:2,ok:false,
2621
},
27-
RecoveryTestVector{
28-
pk: hex!("020000000000000000000000000000000000000000000000000000000000000000"),
29-
msg: hex!("000000000000000000000000000000000000000000000000000CFD5E267CBB5E"),
30-
sig: hex!("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296000000000000000000000000000000000000000000000000000cfd5e267cbb5e"),
31-
recid: 1,
32-
ok: false
22+
RecoveryTestVector{pk:hex!("020000000000000000000000000000000000000000000000000000000000000000"),msg:hex!("000000000000000000000000000000000000000000000000000CFD5E267CBB5E"),sig:hex!("6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296000000000000000000000000000000000000000000000000000cfd5e267cbb5e"),recid:1,ok:false
3323
}];
24+
25+
#[allow(dead_code)]
26+
pub const K256_RECOVERY_TEST_VECTORS: &[RecoveryTestVector] = &[RecoveryTestVector{pk:hex!("020000000000000000000000000000000000000000000000000000000000000000"),msg:hex!("0000000000000000000000000000000000000000000000000000000000000000"),sig:hex!("0000000000000000000000000000000000000000000000000000000000000001ffffffffbffffffffffffffffeffbaffaeff6f7000000100000000dbd0364140"),recid:1,ok:false}
27+
];

0 commit comments

Comments
 (0)