Skip to content

Commit 68c2da2

Browse files
authored
crypto.ecdsa: expand ecdsa module, to support other curves like secp384r1, secp521r1, secp256k1 (#23407)
1 parent 81b421b commit 68c2da2

File tree

6 files changed

+460
-22
lines changed

6 files changed

+460
-22
lines changed

cmd/tools/modules/testing/common.v

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
250250
skip_files << 'examples/websocket/client-server/server.v' // requires OpenSSL
251251
skip_files << 'vlib/v/tests/websocket_logger_interface_should_compile_test.v' // requires OpenSSL
252252
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v' // requires OpenSSL
253+
skip_files << 'vlib/crypto/ecdsa/util_test.v' // requires OpenSSL
253254
$if tinyc {
254255
skip_files << 'examples/database/orm.v' // try fix it
255256
}
@@ -280,13 +281,15 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
280281
}
281282
if github_job == 'docker-ubuntu-musl' {
282283
skip_files << 'vlib/net/openssl/openssl_compiles_test.c.v'
283-
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v'
284+
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v' // requires OpenSSL
285+
skip_files << 'vlib/crypto/ecdsa/util_test.v' // requires OpenSSL
284286
skip_files << 'vlib/x/ttf/ttf_test.v'
285287
skip_files << 'vlib/encoding/iconv/iconv_test.v' // needs libiconv to be installed
286288
}
287289
if github_job == 'tests-sanitize-memory-clang' {
288290
skip_files << 'vlib/net/openssl/openssl_compiles_test.c.v'
289-
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v'
291+
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v' // requires OpenSSL
292+
skip_files << 'vlib/crypto/ecdsa/util_test.v' // requires OpenSSL
290293
// Fails compilation with: `/usr/bin/ld: /lib/x86_64-linux-gnu/libpthread.so.0: error adding symbols: DSO missing from command line`
291294
skip_files << 'examples/sokol/sounds/simple_sin_tones.v'
292295
}

vlib/crypto/ecdsa/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
## ecdsa
2+
3+
`ecdsa` module for V language. Its a wrapper on top of openssl ecdsa functionality.
4+
Its currently (expanded) to support the following curves:
5+
- NIST P-256 curve, commonly referred as prime256v1 or secp256r1
6+
- NIST P-384 curve, commonly referred as secp384r1
7+
- NIST P-521 curve, commonly referred as secp521r1
8+
- A famous Bitcoin curve, commonly referred as secp256k1

vlib/crypto/ecdsa/ecdsa.v

Lines changed: 232 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
// that can be found in the LICENSE file.
44
module ecdsa
55

6+
import hash
7+
import crypto
8+
import crypto.sha256
9+
import crypto.sha512
10+
611
#flag darwin -L /opt/homebrew/opt/openssl/lib -I /opt/homebrew/opt/openssl/include
712

813
#flag -I/usr/include/openssl
@@ -39,9 +44,39 @@ fn C.EC_POINT_cmp(group &C.EC_GROUP, a &C.EC_POINT, b &C.EC_POINT, ctx &C.BN_CTX
3944
fn C.BN_CTX_new() &C.BN_CTX
4045
fn C.BN_CTX_free(ctx &C.BN_CTX)
4146

47+
// for checking the key
48+
fn C.EC_KEY_check_key(key &C.EC_KEY) int
49+
4250
// NID constants
51+
//
52+
// NIST P-256 is refered to as secp256r1 and prime256v1, defined as #define NID_X9_62_prime256v1 415
53+
// Different names, but they are all the same.
54+
// https://www.rfc-editor.org/rfc/rfc4492.html#appendix-A
4355
const nid_prime256v1 = C.NID_X9_62_prime256v1
4456

57+
// NIST P-384, ie, secp384r1 curve, defined as #define NID_secp384r1 715
58+
const nid_secp384r1 = C.NID_secp384r1
59+
60+
// NIST P-521, ie, secp521r1 curve, defined as #define NID_secp521r1 716
61+
const nid_secp521r1 = C.NID_secp521r1
62+
63+
// Bitcoin curve, defined as #define NID_secp256k1 714
64+
const nid_secp256k1 = C.NID_secp256k1
65+
66+
// The list of supported curve(s)
67+
pub enum Nid {
68+
prime256v1
69+
secp384r1
70+
secp521r1
71+
secp256k1
72+
}
73+
74+
@[params]
75+
pub struct CurveOptions {
76+
pub mut:
77+
nid Nid = .prime256v1 // default to NIST P-256 curve
78+
}
79+
4580
@[typedef]
4681
struct C.EC_KEY {}
4782

@@ -68,10 +103,9 @@ pub struct PublicKey {
68103
key &C.EC_KEY
69104
}
70105

71-
// Generate a new key pair
72-
pub fn generate_key() !(PublicKey, PrivateKey) {
73-
nid := nid_prime256v1 // Using NIST P-256 curve
74-
ec_key := C.EC_KEY_new_by_curve_name(nid)
106+
// Generate a new key pair. If opt was not provided, its default to prime256v1 curve.
107+
pub fn generate_key(opt CurveOptions) !(PublicKey, PrivateKey) {
108+
ec_key := new_curve(opt)
75109
if ec_key == 0 {
76110
return error('Failed to create new EC_KEY')
77111
}
@@ -80,6 +114,7 @@ pub fn generate_key() !(PublicKey, PrivateKey) {
80114
C.EC_KEY_free(ec_key)
81115
return error('Failed to generate EC_KEY')
82116
}
117+
83118
priv_key := PrivateKey{
84119
key: ec_key
85120
}
@@ -89,11 +124,10 @@ pub fn generate_key() !(PublicKey, PrivateKey) {
89124
return pub_key, priv_key
90125
}
91126

92-
// Create a new private key from a seed
93-
pub fn new_key_from_seed(seed []u8) !PrivateKey {
94-
nid := nid_prime256v1
127+
// Create a new private key from a seed. If opt was not provided, its default to prime256v1 curve.
128+
pub fn new_key_from_seed(seed []u8, opt CurveOptions) !PrivateKey {
95129
// Create a new EC_KEY object with the specified curve
96-
ec_key := C.EC_KEY_new_by_curve_name(nid)
130+
ec_key := new_curve(opt)
97131
if ec_key == 0 {
98132
return error('Failed to create new EC_KEY')
99133
}
@@ -113,7 +147,16 @@ pub fn new_key_from_seed(seed []u8) !PrivateKey {
113147
// Now compute the public key
114148
//
115149
// Retrieve the EC_GROUP object associated with the EC_KEY
116-
group := C.EC_KEY_get0_group(ec_key)
150+
// Note:
151+
// Its cast-ed with voidptr() to workaround the strictness of the type system,
152+
// ie, cc backend with `-cstrict` option behaviour. Without this cast,
153+
// C.EC_KEY_get0_group expected to return `const EC_GROUP *`,
154+
// ie expected to return pointer into constant of EC_GROUP on C parts,
155+
// so, its make cgen not happy with this and would fail with error.
156+
group := voidptr(C.EC_KEY_get0_group(ec_key))
157+
if group == 0 {
158+
return error('failed to load group')
159+
}
117160
// Create a new EC_POINT object for the public key
118161
pub_key_point := C.EC_POINT_new(group)
119162
// Create a new BN_CTX object for efficient BIGNUM operations
@@ -143,6 +186,13 @@ pub fn new_key_from_seed(seed []u8) !PrivateKey {
143186
C.EC_KEY_free(ec_key)
144187
return error('Failed to set public key')
145188
}
189+
// Add key check
190+
// EC_KEY_check_key return 1 on success or 0 on error.
191+
chk := C.EC_KEY_check_key(ec_key)
192+
if chk == 0 {
193+
key_free(ec_key)
194+
return error('EC_KEY_check_key failed')
195+
}
146196
C.EC_POINT_free(pub_key_point)
147197
C.BN_free(bn)
148198
return PrivateKey{
@@ -151,6 +201,7 @@ pub fn new_key_from_seed(seed []u8) !PrivateKey {
151201
}
152202

153203
// Sign a message with private key
204+
// FIXME: should the message should be hashed?
154205
pub fn (priv_key PrivateKey) sign(message []u8) ![]u8 {
155206
if message.len == 0 {
156207
return error('Message cannot be null or empty')
@@ -179,7 +230,7 @@ pub fn (pub_key PublicKey) verify(message []u8, sig []u8) !bool {
179230

180231
// Get the seed (private key bytes)
181232
pub fn (priv_key PrivateKey) seed() ![]u8 {
182-
bn := C.EC_KEY_get0_private_key(priv_key.key)
233+
bn := voidptr(C.EC_KEY_get0_private_key(priv_key.key))
183234
if bn == 0 {
184235
return error('Failed to get private key BIGNUM')
185236
}
@@ -204,28 +255,189 @@ pub fn (priv_key PrivateKey) public_key() !PublicKey {
204255
}
205256
}
206257

207-
// Compare two private keys
258+
// EC_GROUP_cmp() for comparing two group (curve).
259+
// EC_GROUP_cmp returns 0 if the curves are equal, 1 if they are not equal, or -1 on error.
260+
fn C.EC_GROUP_cmp(a &C.EC_GROUP, b &C.EC_GROUP, ctx &C.BN_CTX) int
261+
262+
// equal compares two private keys was equal. Its checks for two things, ie:
263+
// - whether both of private keys lives under the same group (curve)
264+
// - compares if two private key bytes was equal
208265
pub fn (priv_key PrivateKey) equal(other PrivateKey) bool {
209-
bn1 := C.EC_KEY_get0_private_key(priv_key.key)
210-
bn2 := C.EC_KEY_get0_private_key(other.key)
211-
res := C.BN_cmp(bn1, bn2)
212-
return res == 0
266+
group1 := voidptr(C.EC_KEY_get0_group(priv_key.key))
267+
group2 := voidptr(C.EC_KEY_get0_group(other.key))
268+
ctx := C.BN_CTX_new()
269+
if ctx == 0 {
270+
return false
271+
}
272+
defer {
273+
C.BN_CTX_free(ctx)
274+
}
275+
gres := C.EC_GROUP_cmp(group1, group2, ctx)
276+
// Its lives on the same group
277+
if gres == 0 {
278+
bn1 := voidptr(C.EC_KEY_get0_private_key(priv_key.key))
279+
bn2 := voidptr(C.EC_KEY_get0_private_key(other.key))
280+
res := C.BN_cmp(bn1, bn2)
281+
return res == 0
282+
}
283+
return false
213284
}
214285

215286
// Compare two public keys
216287
pub fn (pub_key PublicKey) equal(other PublicKey) bool {
217-
group := C.EC_KEY_get0_group(pub_key.key)
218-
point1 := C.EC_KEY_get0_public_key(pub_key.key)
219-
point2 := C.EC_KEY_get0_public_key(other.key)
288+
// TODO: check validity of the group
289+
group1 := voidptr(C.EC_KEY_get0_group(pub_key.key))
290+
group2 := voidptr(C.EC_KEY_get0_group(other.key))
291+
if group1 == 0 || group2 == 0 {
292+
return false
293+
}
220294
ctx := C.BN_CTX_new()
221295
if ctx == 0 {
222296
return false
223297
}
224298
defer {
225299
C.BN_CTX_free(ctx)
226300
}
227-
res := C.EC_POINT_cmp(group, point1, point2, ctx)
228-
return res == 0
301+
gres := C.EC_GROUP_cmp(group1, group2, ctx)
302+
// Its lives on the same group
303+
if gres == 0 {
304+
point1 := voidptr(C.EC_KEY_get0_public_key(pub_key.key))
305+
point2 := voidptr(C.EC_KEY_get0_public_key(other.key))
306+
if point1 == 0 || point2 == 0 {
307+
return false
308+
}
309+
res := C.EC_POINT_cmp(group1, point1, point2, ctx)
310+
return res == 0
311+
}
312+
313+
return false
314+
}
315+
316+
// Helpers
317+
//
318+
// new_curve creates a new empty curve based on curve NID, default to prime256v1 (or secp256r1).
319+
fn new_curve(opt CurveOptions) &C.EC_KEY {
320+
mut nid := nid_prime256v1
321+
match opt.nid {
322+
.prime256v1 {
323+
// do nothing
324+
}
325+
.secp384r1 {
326+
nid = nid_secp384r1
327+
}
328+
.secp521r1 {
329+
nid = nid_secp521r1
330+
}
331+
.secp256k1 {
332+
nid = nid_secp256k1
333+
}
334+
}
335+
return C.EC_KEY_new_by_curve_name(nid)
336+
}
337+
338+
// Gets recommended hash function of the current PrivateKey.
339+
// Its purposes for hashing message to be signed
340+
fn (pv PrivateKey) recommended_hash() !crypto.Hash {
341+
group := voidptr(C.EC_KEY_get0_group(pv.key))
342+
if group == 0 {
343+
return error('Unable to load group')
344+
}
345+
// gets the bits size of private key group
346+
num_bits := C.EC_GROUP_get_degree(group)
347+
match true {
348+
// use sha256
349+
num_bits <= 256 {
350+
return .sha256
351+
}
352+
num_bits > 256 && num_bits <= 384 {
353+
return .sha384
354+
}
355+
// TODO: what hash should be used if the size is over > 512 bits
356+
num_bits > 384 {
357+
return .sha512
358+
}
359+
else {
360+
return error('Unsupported bits size')
361+
}
362+
}
363+
}
364+
365+
pub enum HashConfig {
366+
with_recomended_hash
367+
with_no_hash
368+
with_custom_hash
369+
}
370+
371+
@[params]
372+
pub struct SignerOpts {
373+
pub mut:
374+
hash_config HashConfig = .with_recomended_hash
375+
// make sense when HashConfig != with_recomended_hash
376+
allow_smaller_size bool
377+
allow_custom_hash bool
378+
// set to non-nil if allow_custom_hash was true
379+
custom_hash &hash.Hash = unsafe { nil }
380+
}
381+
382+
// sign_with_options sign the message with the options. By default, it would precompute
383+
// hash value from message, with recommended_hash function, and then sign the hash value.
384+
pub fn (pv PrivateKey) sign_with_options(message []u8, opts SignerOpts) ![]u8 {
385+
// we're working on mutable copy of SignerOpts, with some issues when make it as a mutable.
386+
// ie, declaring a mutable parameter that accepts a struct with the `@[params]` attribute is not allowed.
387+
mut cfg := opts
388+
match cfg.hash_config {
389+
.with_recomended_hash {
390+
h := pv.recommended_hash()!
391+
match h {
392+
.sha256 {
393+
digest := sha256.sum256(message)
394+
return pv.sign(digest)!
395+
}
396+
.sha384 {
397+
digest := sha512.sum384(message)
398+
return pv.sign(digest)!
399+
}
400+
.sha512 {
401+
digest := sha512.sum512(message)
402+
return pv.sign(digest)!
403+
}
404+
else {
405+
return error('Unsupported hash')
406+
}
407+
}
408+
}
409+
.with_no_hash {
410+
return pv.sign(message)!
411+
}
412+
.with_custom_hash {
413+
if !cfg.allow_custom_hash {
414+
return error('custom hash was not allowed, set it into true')
415+
}
416+
if cfg.custom_hash == unsafe { nil } {
417+
return error('Custom hasher was not defined')
418+
}
419+
// check key size bits
420+
group := voidptr(C.EC_KEY_get0_group(pv.key))
421+
if group == 0 {
422+
return error('fail to load group')
423+
}
424+
num_bits := C.EC_GROUP_get_degree(group)
425+
// check for key size matching
426+
key_size := (num_bits + 7) / 8
427+
// If current Private Key size is bigger then current hash output size,
428+
// by default its not allowed, until set the allow_smaller_size into true
429+
if key_size > cfg.custom_hash.size() {
430+
if !cfg.allow_smaller_size {
431+
return error('Hash into smaller size than current key size was not allowed')
432+
}
433+
}
434+
// otherwise, just hash the message and sign
435+
digest := cfg.custom_hash.sum(message)
436+
defer { unsafe { cfg.custom_hash.free() } }
437+
return pv.sign(digest)!
438+
}
439+
}
440+
return error('Not should be here')
229441
}
230442

231443
// Clear allocated memory for key

0 commit comments

Comments
 (0)