Skip to content

Commit 7991a67

Browse files
committed
Add ECDSA public key recovery
Added algorithm for recovering public key from ECDSA signature.
1 parent 03c8e50 commit 7991a67

File tree

5 files changed

+309
-12
lines changed

5 files changed

+309
-12
lines changed

include/ack/ec.hpp

+5-3
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,7 @@ namespace ack {
614614
template<typename CurveT>
615615
struct ec_point_fp_proj : ec_point_base<ec_point_fp_proj<CurveT>, CurveT>
616616
{
617-
static_assert( is_ec_curve_fp<CurveT> );
617+
static_assert( is_ec_curve_fp<std::remove_cv_t<CurveT>> );
618618

619619
using base_type = ec_point_base<ec_point_fp_proj<CurveT>, CurveT>;
620620
using int_type = typename CurveT::int_type;
@@ -969,7 +969,7 @@ namespace ack {
969969
template<typename CurveT>
970970
struct ec_point_fp_jacobi : ec_point_base<ec_point_fp_jacobi<CurveT>, CurveT>
971971
{
972-
static_assert( is_ec_curve_fp<CurveT> );
972+
static_assert( is_ec_curve_fp<std::remove_cv_t<CurveT>> );
973973

974974
using base_type = ec_point_base<ec_point_fp_jacobi<CurveT>, CurveT>;
975975
using int_type = typename CurveT::int_type;
@@ -1528,6 +1528,7 @@ namespace ack {
15281528
// #E(Fp) - number of points on the curve
15291529
const bool a_is_minus_3; // cached a == p - 3
15301530
const bool a_is_zero; // cached a == 0
1531+
const IntT p_minus_n; // cached p - n; used for checking the maximum negative point coordinate
15311532

15321533
/**
15331534
* Creates a curve from the given parameters.
@@ -1546,7 +1547,8 @@ namespace ack {
15461547
n( std::move(n) ),
15471548
h( h ),
15481549
a_is_minus_3( this->a == ( this->p - 3) ),
1549-
a_is_zero( this->a.is_zero() )
1550+
a_is_zero( this->a.is_zero() ),
1551+
p_minus_n( this->p - this->n )
15501552
{}
15511553

15521554
/**

include/ack/ecdsa.hpp

+110-9
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ namespace ack{
1616
* @tparam CurveT - elliptic curve type. Required to be a curve over prime field.
1717
* @tparam IntT - integer type, default is curve integer type
1818
*
19-
* @param q - public key point. Note: point validity should be checked before calling this function.
20-
* @param msg - message to verify. Note: msg is truncated to curve.n byte length.
21-
* @param r - signature r value
22-
* @param s - signature s value
19+
* @param q - public key point. Note: point validity should be checked before calling this function.
20+
* @param msg - message to verify. Note: msg is truncated to curve.n byte length.
21+
* @param r - signature r value
22+
* @param s - signature s value
2323
* @return true if signature is valid, false otherwise.
2424
*/
2525
template<typename CurveT, typename IntT = typename CurveT::int_type>
@@ -42,18 +42,18 @@ namespace ack{
4242
const IntT w = s.modinv( curve.n );
4343
const auto u1 = ( e * w ) % curve.n;
4444
const auto u2 = ( r * w ) % curve.n;
45-
auto rr = ec_mul_add_fast( u1, ec_point_fp_jacobi( curve.g ), u2, ec_point_fp_jacobi( q ) )
45+
auto R = ec_mul_add_fast( u1, ec_point_fp_jacobi( curve.g ), u2, ec_point_fp_jacobi( q ) )
4646
.to_affine();
4747

48-
if ( rr.is_identity() ) {
48+
if ( R.is_identity() ) {
4949
return false;
5050
}
5151

52-
if ( rr.x >= curve.n ) {
53-
rr.x -= curve.n;
52+
if ( R.x >= curve.n ) {
53+
R.x -= curve.n;
5454
}
5555

56-
return rr.x == r;
56+
return R.x == r;
5757
}
5858

5959
/**
@@ -116,4 +116,105 @@ namespace ack{
116116
{
117117
check( ecdsa_verify( q, digest, r, s ), error );
118118
}
119+
120+
/**
121+
* Function recovers public key from given message and ECDSA signature.
122+
* Implementation is modified version of SEC 1 v2.0, section 4.1.6 'Public Key Recovery Operation':
123+
* https://www.secg.org/sec1-v2.pdf#page=53
124+
*
125+
* @note This function currently works only for curve.h == 1.
126+
*
127+
* @tparam CurveT - elliptic curve type. Required to be a curve over prime field.
128+
* @tparam IntT - integer type, default is curve integer type
129+
*
130+
* @param curve - Elliptic curve
131+
* @param msg - message
132+
* @param r - signature r value
133+
* @param s - signature s value
134+
* @param recid - recovery id of the public key Q. (recid <= 3)
135+
* @param verify - Performs additional verifications
136+
*
137+
* @return recovered EC public key point Q, or point at infinity if Q can't be calculated.
138+
*/
139+
template<typename CurveT, typename IntT = typename CurveT::int_type>
140+
[[nodiscard]]
141+
static ec_point_fp<CurveT> ecdsa_recover(const CurveT& curve, const bytes_view msg, const IntT& r, const IntT& s,
142+
const std::size_t recid, const bool verify = false)
143+
{
144+
if ( r < 1 || r >= curve.n || s < 1 || s >= curve.n || ( recid & 3 ) != recid ) {
145+
return ec_point_fp<CurveT>();
146+
}
147+
148+
const auto yodd = recid & 1;
149+
const auto second_key = recid >> 1;
150+
151+
// Sanity check, the second key is expected to be negative
152+
if ( second_key && r >= curve.p_minus_n ) {
153+
return ec_point_fp<CurveT>();
154+
}
155+
156+
auto e = IntT( msg.subspan( 0, std::min( curve.n.byte_length(), msg.size() )));
157+
if ( e > curve.n ) {
158+
e -= curve.n;
159+
}
160+
161+
// Calculate R, 1.1 - 1.3
162+
// 1.1. Let x = r + jn.
163+
const auto R = (second_key)
164+
? curve.decompress_point( r + curve.n, yodd )
165+
: curve.decompress_point( r, yodd );
166+
167+
if ( R.is_identity() ) {
168+
return R;
169+
}
170+
171+
if ( verify ) {
172+
// 1.4 nR == O
173+
if ( !( R * curve.n ).is_identity() ) {
174+
return ec_point_fp<CurveT>();
175+
}
176+
}
177+
178+
// 1.6.1 Compute Q = r^-1 (sR - eG)
179+
// Q = r^-1 (sR + -eG)
180+
//
181+
// We precompute w = r^-1, ew = -ew and sw = sw
182+
// so that we can then efficiently compute: Q = ew * G + sw * R
183+
const IntT w = r.modinv( curve.n );
184+
const auto ew = (( curve.n - e ) * w ) % curve.n;
185+
const auto sw = ( s * w ) % curve.n;
186+
return ec_mul_add_fast( ew, ec_point_fp_jacobi( curve.g ), sw, ec_point_fp_jacobi( R ) )
187+
.to_affine();
188+
}
189+
190+
/**
191+
* Function recovers public key from given message and ECDSA signature.
192+
* Implementation is modified version of SEC 1 v2.0, section 4.1.6 'Public Key Recovery Operation':
193+
* https://www.secg.org/sec1-v2.pdf#page=53
194+
*
195+
* @note function works currently only for curve.h = 1
196+
*
197+
* @tparam HLen - digest length
198+
* @tparam CurveT - elliptic curve type. Required to be a curve over prime field.
199+
* @tparam IntT - integer type, default is curve integer type
200+
*
201+
* @param curve - Elliptic curve
202+
* @param digest - message digest. Note: digest is truncated to curve.n byte length.
203+
* @param r - signature r value
204+
* @param s - signature s value
205+
* @param recid - recovery id of the public key Q. (recid <= 3)
206+
* @param verify - Performs additional verifications
207+
*
208+
* @return recovered EC public key point Q, or point at infinity if Q can't be calculated.
209+
*/
210+
template<std::size_t HLen, typename CurveT, typename IntT = typename CurveT::int_type>
211+
[[nodiscard]]
212+
static ec_point_fp<CurveT> ecdsa_recover(const CurveT& curve, const hash_t<HLen>& digest,
213+
const IntT& r, const IntT& s,
214+
const std::size_t recid, const bool verify = false)
215+
{
216+
const auto bd = digest.extract_as_byte_array();
217+
const auto m = bytes_view( reinterpret_cast<const byte_t*>( bd.data() ), HLen );
218+
return ecdsa_recover( curve, m, r, s, recid, verify );
219+
}
119220
}
+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
// Copyright © 2023 ZeroPass <[email protected]>
2+
// Author: Crt Vavros
3+
#pragma once
4+
#include <ack/ec.hpp>
5+
#include <ack/ec_curve.hpp>
6+
#include <ack/ecdsa.hpp>
7+
#include <ack/keccak.hpp>
8+
#include <ack/sha.hpp>
9+
#include <ack/utils.hpp>
10+
#include <ack/tests/ecdsa_test_utils.hpp>
11+
#include <ack/tests/utils.hpp>
12+
13+
#include <eosio/crypto.hpp>
14+
#include <eosio/tester.hpp>
15+
16+
17+
namespace ack::tests {
18+
EOSIO_TEST_BEGIN( ecdsa_misc_test )
19+
constexpr auto& curve = ack::ec_curve::secp256r1;
20+
using point_type = std::remove_cvref_t<decltype(curve)>::point_type;
21+
using int_type = std::remove_cvref_t<decltype(curve)>::int_type;
22+
23+
constexpr auto q = curve.make_point(
24+
"e424dc61d4bb3cb7ef4344a7f8957a0c5134e16f7a67c074f82e6e12f49abf3c",
25+
"970eed7aa2bc48651545949de1dddaf0127e5965ac85d1243d6f60e7dfaee927"
26+
);
27+
28+
constexpr auto h = from_hex( "d1b8ef21eb4182ee270638061063a3f3c16c114e33937f69fb232cc833965a94" );
29+
auto md = hash256( h );
30+
constexpr int_type r = "bf96b99aa49c705c910be33142017c642ff540c76349b9dab72f981fd9347f4f";
31+
constexpr int_type s = "17c55095819089c2e03b9cd415abdf12444e323075d98f31920b9e0f57ec871c";
32+
constexpr size_t recid = 1;
33+
34+
// Misc ECDSA key recovery tests
35+
{
36+
// Test recovery succeeds
37+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, r, s, recid, /*verify=*/true ), q )
38+
39+
// Test passing too big recid results in point at infinity
40+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, r, s, /*recid=*/4, /*verify=*/true ).is_identity(), true )
41+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, r, s, /*recid=*/4, /*verify=*/true ).is_identity(), true )
42+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, r, s, /*recid=*/0xff, /*verify=*/true ).is_identity(), true )
43+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, r, s, /*recid=*/0xff, /*verify=*/true ).is_identity(), true )
44+
45+
// Test passing too small and too big r
46+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, int_type( 0 ), s, recid, /*verify=*/true ).is_identity(), true )
47+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, int_type( 0 ), s, recid, /*verify=*/true ).is_identity(), true )
48+
49+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, curve.n, s, recid, /*verify=*/true ).is_identity(), true )
50+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, curve.n, s, recid, /*verify=*/true ).is_identity(), true )
51+
52+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, curve.n + 1, s, recid, /*verify=*/true ).is_identity(), true )
53+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, curve.n + 1, s, recid, /*verify=*/true ).is_identity(), true )
54+
55+
// Test passing too small and too big s
56+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, r, int_type( 0 ), recid, /*verify=*/true ).is_identity(), true )
57+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, r, int_type( 0 ), recid, /*verify=*/true ).is_identity(), true )
58+
59+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, r, curve.n, recid, /*verify=*/true ).is_identity(), true )
60+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, r, curve.n, recid, /*verify=*/true ).is_identity(), true )
61+
62+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, r, curve.n + 1, recid, /*verify=*/true ).is_identity(), true )
63+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, r, curve.n + 1, recid, /*verify=*/true ).is_identity(), true )
64+
65+
// Test passing too big r when recovering second key
66+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, curve.p_minus_n, s, 2, /*verify=*/true ).is_identity(), true )
67+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, curve.p_minus_n, s, 2, /*verify=*/true ).is_identity(), true )
68+
69+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), h, curve.p_minus_n + 1, s, 2, /*verify=*/true ).is_identity(), true )
70+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), md, curve.p_minus_n + 1, s, 2, /*verify=*/true ).is_identity(), true )
71+
}
72+
73+
// Misc ECDSA sigver tests
74+
{
75+
// Test verification ECDSA signature succeeds
76+
REQUIRE_EQUAL( ecdsa_verify( q, h, r, s ), true )
77+
REQUIRE_EQUAL( ecdsa_verify( q, md, r, s ), true )
78+
assert_ecdsa( q, h, r, s,
79+
"ECDSA signature verification failed"
80+
);
81+
assert_ecdsa( q, md, r, s,
82+
"ECDSA signature verification failed"
83+
);
84+
85+
// Test verification fails when passing identity q
86+
REQUIRE_EQUAL( ecdsa_verify( point_type{}, h, r, s ), false )
87+
REQUIRE_EQUAL( ecdsa_verify( point_type{}, md, r, s ), false )
88+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
89+
assert_ecdsa( point_type{}, h, r, s,
90+
"ECDSA signature verification failed"
91+
);
92+
})
93+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
94+
assert_ecdsa( point_type{}, md, r, s,
95+
"ECDSA signature verification failed"
96+
);
97+
})
98+
99+
// Test passing too small and too big r
100+
REQUIRE_EQUAL( ecdsa_verify( q, h, int_type( 0 ), s ), false )
101+
REQUIRE_EQUAL( ecdsa_verify( q, md, int_type( 0 ), s ), false )
102+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
103+
assert_ecdsa( q, h, int_type( 0 ), s,
104+
"ECDSA signature verification failed"
105+
);
106+
})
107+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
108+
assert_ecdsa( q, md, int_type( 0 ), s,
109+
"ECDSA signature verification failed"
110+
);
111+
})
112+
113+
REQUIRE_EQUAL( ecdsa_verify( q, h, curve.n, s ), false )
114+
REQUIRE_EQUAL( ecdsa_verify( q, md, curve.n, s ), false )
115+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
116+
assert_ecdsa( q, h, curve.n, s,
117+
"ECDSA signature verification failed"
118+
);
119+
})
120+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
121+
assert_ecdsa( q, md, curve.n, s,
122+
"ECDSA signature verification failed"
123+
);
124+
})
125+
126+
REQUIRE_EQUAL( ecdsa_verify( q, h, curve.n + 1, s ), false )
127+
REQUIRE_EQUAL( ecdsa_verify( q, md, curve.n + 1, s ), false )
128+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
129+
assert_ecdsa( q, h, curve.n + 1, s,
130+
"ECDSA signature verification failed"
131+
);
132+
})
133+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
134+
assert_ecdsa( q, md, curve.n + 1, s,
135+
"ECDSA signature verification failed"
136+
);
137+
})
138+
139+
// Test passing too small and too big s
140+
REQUIRE_EQUAL( ecdsa_verify( q, h, r, int_type( 0 ) ), false )
141+
REQUIRE_EQUAL( ecdsa_verify( q, md, r, int_type( 0 ) ), false )
142+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
143+
assert_ecdsa( q, h, r, int_type( 0 ),
144+
"ECDSA signature verification failed"
145+
);
146+
})
147+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
148+
assert_ecdsa( q, md, r, int_type( 0 ),
149+
"ECDSA signature verification failed"
150+
);
151+
})
152+
153+
REQUIRE_EQUAL( ecdsa_verify( q, h, r, curve.n ), false )
154+
REQUIRE_EQUAL( ecdsa_verify( q, md, r, curve.n ), false )
155+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
156+
assert_ecdsa( q, h, r, curve.n,
157+
"ECDSA signature verification failed"
158+
);
159+
})
160+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
161+
assert_ecdsa( q, md, r, curve.n,
162+
"ECDSA signature verification failed"
163+
);
164+
})
165+
166+
REQUIRE_EQUAL( ecdsa_verify( q, h, r, curve.n + 1 ), false )
167+
REQUIRE_EQUAL( ecdsa_verify( q, md, r, curve.n + 1 ), false )
168+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
169+
assert_ecdsa( q, h, r, curve.n + 1,
170+
"ECDSA signature verification failed"
171+
);
172+
})
173+
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {
174+
assert_ecdsa( q, md, r, curve.n + 1,
175+
"ECDSA signature verification failed"
176+
);
177+
})
178+
}
179+
EOSIO_TEST_END
180+
}

tests/include/ack/tests/ecdsa_test.hpp

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <ack/tests/ecdsa_brainpoolP320r1_test.hpp>
66
#include <ack/tests/ecdsa_brainpoolP384r1_test.hpp>
77
#include <ack/tests/ecdsa_brainpoolP512r1_test.hpp>
8+
#include <ack/tests/ecdsa_misc_test.hpp>
89
#include <ack/tests/ecdsa_secp256k1_test.hpp>
910
#include <ack/tests/ecdsa_secp256r1_test.hpp>
1011
#include <ack/tests/ecdsa_secp384r1_test.hpp>
@@ -14,6 +15,7 @@
1415

1516
namespace ack::tests {
1617
EOSIO_TEST_BEGIN( ecdsa_test )
18+
EOSIO_TEST( ecdsa_misc_test )
1719
EOSIO_TEST( ecdsa_brainpoolP256r1_test )
1820
EOSIO_TEST( ecdsa_brainpoolP320r1_test )
1921
EOSIO_TEST( ecdsa_brainpoolP384r1_test )

tests/include/ack/tests/ecdsa_test_utils.hpp

+12
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ namespace ack::tests {
3535
"ECDSA signature verification failed"
3636
);
3737
})
38+
39+
// test recovery
40+
bool recovered = false;
41+
for (std::size_t i = 0; i < 4; i ++) {
42+
if ( ecdsa_recover( q.curve(), hb, r, s, i, /*verify=*/true ) == q ) {
43+
REQUIRE_EQUAL( ecdsa_recover( q.curve(), digest, r, s, i, /*verify=*/true ), q )
44+
recovered = true;
45+
break;
46+
}
47+
}
48+
49+
REQUIRE_EQUAL( recovered, true )
3850
}
3951
else {
4052
REQUIRE_ASSERT( "ECDSA signature verification failed", [&]() {

0 commit comments

Comments
 (0)