Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto.ecdsa: expand ecdsa module to support another curve. #23407

Merged
merged 4 commits into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions cmd/tools/modules/testing/common.v
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
skip_files << 'examples/websocket/client-server/server.v' // requires OpenSSL
skip_files << 'vlib/v/tests/websocket_logger_interface_should_compile_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/util_test.v' // requires OpenSSL
$if tinyc {
skip_files << 'examples/database/orm.v' // try fix it
}
Expand Down Expand Up @@ -280,13 +281,15 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
}
if github_job == 'docker-ubuntu-musl' {
skip_files << 'vlib/net/openssl/openssl_compiles_test.c.v'
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v'
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/util_test.v' // requires OpenSSL
skip_files << 'vlib/x/ttf/ttf_test.v'
skip_files << 'vlib/encoding/iconv/iconv_test.v' // needs libiconv to be installed
}
if github_job == 'tests-sanitize-memory-clang' {
skip_files << 'vlib/net/openssl/openssl_compiles_test.c.v'
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v'
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/util_test.v' // requires OpenSSL
// Fails compilation with: `/usr/bin/ld: /lib/x86_64-linux-gnu/libpthread.so.0: error adding symbols: DSO missing from command line`
skip_files << 'examples/sokol/sounds/simple_sin_tones.v'
}
Expand Down
8 changes: 8 additions & 0 deletions vlib/crypto/ecdsa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
## ecdsa

`ecdsa` module for V language. Its a wrapper on top of openssl ecdsa functionality.
Its currently (expanded) to support the following curves:
- NIST P-256 curve, commonly referred as prime256v1 or secp256r1
- NIST P-384 curve, commonly referred as secp384r1
- NIST P-521 curve, commonly referred as secp521r1
- A famous Bitcoin curve, commonly referred as secp256k1
252 changes: 232 additions & 20 deletions vlib/crypto/ecdsa/ecdsa.v
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
// that can be found in the LICENSE file.
module ecdsa

import hash
import crypto
import crypto.sha256
import crypto.sha512

#flag darwin -L /opt/homebrew/opt/openssl/lib -I /opt/homebrew/opt/openssl/include

#flag -I/usr/include/openssl
Expand Down Expand Up @@ -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
fn C.BN_CTX_new() &C.BN_CTX
fn C.BN_CTX_free(ctx &C.BN_CTX)

// for checking the key
fn C.EC_KEY_check_key(key &C.EC_KEY) int

// NID constants
//
// NIST P-256 is refered to as secp256r1 and prime256v1, defined as #define NID_X9_62_prime256v1 415
// Different names, but they are all the same.
// https://www.rfc-editor.org/rfc/rfc4492.html#appendix-A
const nid_prime256v1 = C.NID_X9_62_prime256v1

// NIST P-384, ie, secp384r1 curve, defined as #define NID_secp384r1 715
const nid_secp384r1 = C.NID_secp384r1

// NIST P-521, ie, secp521r1 curve, defined as #define NID_secp521r1 716
const nid_secp521r1 = C.NID_secp521r1

// Bitcoin curve, defined as #define NID_secp256k1 714
const nid_secp256k1 = C.NID_secp256k1

// The list of supported curve(s)
pub enum Nid {
prime256v1
secp384r1
secp521r1
secp256k1
}

@[params]
pub struct CurveOptions {
pub mut:
nid Nid = .prime256v1 // default to NIST P-256 curve
}

@[typedef]
struct C.EC_KEY {}

Expand All @@ -68,10 +103,9 @@ pub struct PublicKey {
key &C.EC_KEY
}

// Generate a new key pair
pub fn generate_key() !(PublicKey, PrivateKey) {
nid := nid_prime256v1 // Using NIST P-256 curve
ec_key := C.EC_KEY_new_by_curve_name(nid)
// Generate a new key pair. If opt was not provided, its default to prime256v1 curve.
pub fn generate_key(opt CurveOptions) !(PublicKey, PrivateKey) {
ec_key := new_curve(opt)
if ec_key == 0 {
return error('Failed to create new EC_KEY')
}
Expand All @@ -80,6 +114,7 @@ pub fn generate_key() !(PublicKey, PrivateKey) {
C.EC_KEY_free(ec_key)
return error('Failed to generate EC_KEY')
}

priv_key := PrivateKey{
key: ec_key
}
Expand All @@ -89,11 +124,10 @@ pub fn generate_key() !(PublicKey, PrivateKey) {
return pub_key, priv_key
}

// Create a new private key from a seed
pub fn new_key_from_seed(seed []u8) !PrivateKey {
nid := nid_prime256v1
// Create a new private key from a seed. If opt was not provided, its default to prime256v1 curve.
pub fn new_key_from_seed(seed []u8, opt CurveOptions) !PrivateKey {
// Create a new EC_KEY object with the specified curve
ec_key := C.EC_KEY_new_by_curve_name(nid)
ec_key := new_curve(opt)
if ec_key == 0 {
return error('Failed to create new EC_KEY')
}
Expand All @@ -113,7 +147,16 @@ pub fn new_key_from_seed(seed []u8) !PrivateKey {
// Now compute the public key
//
// Retrieve the EC_GROUP object associated with the EC_KEY
group := C.EC_KEY_get0_group(ec_key)
// Note:
// Its cast-ed with voidptr() to workaround the strictness of the type system,
// ie, cc backend with `-cstrict` option behaviour. Without this cast,
// C.EC_KEY_get0_group expected to return `const EC_GROUP *`,
// ie expected to return pointer into constant of EC_GROUP on C parts,
// so, its make cgen not happy with this and would fail with error.
group := voidptr(C.EC_KEY_get0_group(ec_key))
if group == 0 {
return error('failed to load group')
}
// Create a new EC_POINT object for the public key
pub_key_point := C.EC_POINT_new(group)
// Create a new BN_CTX object for efficient BIGNUM operations
Expand Down Expand Up @@ -143,6 +186,13 @@ pub fn new_key_from_seed(seed []u8) !PrivateKey {
C.EC_KEY_free(ec_key)
return error('Failed to set public key')
}
// Add key check
// EC_KEY_check_key return 1 on success or 0 on error.
chk := C.EC_KEY_check_key(ec_key)
if chk == 0 {
key_free(ec_key)
return error('EC_KEY_check_key failed')
}
C.EC_POINT_free(pub_key_point)
C.BN_free(bn)
return PrivateKey{
Expand All @@ -151,6 +201,7 @@ pub fn new_key_from_seed(seed []u8) !PrivateKey {
}

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

// Get the seed (private key bytes)
pub fn (priv_key PrivateKey) seed() ![]u8 {
bn := C.EC_KEY_get0_private_key(priv_key.key)
bn := voidptr(C.EC_KEY_get0_private_key(priv_key.key))
if bn == 0 {
return error('Failed to get private key BIGNUM')
}
Expand All @@ -204,28 +255,189 @@ pub fn (priv_key PrivateKey) public_key() !PublicKey {
}
}

// Compare two private keys
// EC_GROUP_cmp() for comparing two group (curve).
// EC_GROUP_cmp returns 0 if the curves are equal, 1 if they are not equal, or -1 on error.
fn C.EC_GROUP_cmp(a &C.EC_GROUP, b &C.EC_GROUP, ctx &C.BN_CTX) int

// equal compares two private keys was equal. Its checks for two things, ie:
// - whether both of private keys lives under the same group (curve)
// - compares if two private key bytes was equal
pub fn (priv_key PrivateKey) equal(other PrivateKey) bool {
bn1 := C.EC_KEY_get0_private_key(priv_key.key)
bn2 := C.EC_KEY_get0_private_key(other.key)
res := C.BN_cmp(bn1, bn2)
return res == 0
group1 := voidptr(C.EC_KEY_get0_group(priv_key.key))
group2 := voidptr(C.EC_KEY_get0_group(other.key))
ctx := C.BN_CTX_new()
if ctx == 0 {
return false
}
defer {
C.BN_CTX_free(ctx)
}
gres := C.EC_GROUP_cmp(group1, group2, ctx)
// Its lives on the same group
if gres == 0 {
bn1 := voidptr(C.EC_KEY_get0_private_key(priv_key.key))
bn2 := voidptr(C.EC_KEY_get0_private_key(other.key))
res := C.BN_cmp(bn1, bn2)
return res == 0
}
return false
}

// Compare two public keys
pub fn (pub_key PublicKey) equal(other PublicKey) bool {
group := C.EC_KEY_get0_group(pub_key.key)
point1 := C.EC_KEY_get0_public_key(pub_key.key)
point2 := C.EC_KEY_get0_public_key(other.key)
// TODO: check validity of the group
group1 := voidptr(C.EC_KEY_get0_group(pub_key.key))
group2 := voidptr(C.EC_KEY_get0_group(other.key))
if group1 == 0 || group2 == 0 {
return false
}
ctx := C.BN_CTX_new()
if ctx == 0 {
return false
}
defer {
C.BN_CTX_free(ctx)
}
res := C.EC_POINT_cmp(group, point1, point2, ctx)
return res == 0
gres := C.EC_GROUP_cmp(group1, group2, ctx)
// Its lives on the same group
if gres == 0 {
point1 := voidptr(C.EC_KEY_get0_public_key(pub_key.key))
point2 := voidptr(C.EC_KEY_get0_public_key(other.key))
if point1 == 0 || point2 == 0 {
return false
}
res := C.EC_POINT_cmp(group1, point1, point2, ctx)
return res == 0
}

return false
}

// Helpers
//
// new_curve creates a new empty curve based on curve NID, default to prime256v1 (or secp256r1).
fn new_curve(opt CurveOptions) &C.EC_KEY {
mut nid := nid_prime256v1
match opt.nid {
.prime256v1 {
// do nothing
}
.secp384r1 {
nid = nid_secp384r1
}
.secp521r1 {
nid = nid_secp521r1
}
.secp256k1 {
nid = nid_secp256k1
}
}
return C.EC_KEY_new_by_curve_name(nid)
}

// Gets recommended hash function of the current PrivateKey.
// Its purposes for hashing message to be signed
fn (pv PrivateKey) recommended_hash() !crypto.Hash {
group := voidptr(C.EC_KEY_get0_group(pv.key))
if group == 0 {
return error('Unable to load group')
}
// gets the bits size of private key group
num_bits := C.EC_GROUP_get_degree(group)
match true {
// use sha256
num_bits <= 256 {
return .sha256
}
num_bits > 256 && num_bits <= 384 {
return .sha384
}
// TODO: what hash should be used if the size is over > 512 bits
num_bits > 384 {
return .sha512
}
else {
return error('Unsupported bits size')
}
}
}

pub enum HashConfig {
with_recomended_hash
with_no_hash
with_custom_hash
}

@[params]
pub struct SignerOpts {
pub mut:
hash_config HashConfig = .with_recomended_hash
// make sense when HashConfig != with_recomended_hash
allow_smaller_size bool
allow_custom_hash bool
// set to non-nil if allow_custom_hash was true
custom_hash &hash.Hash = unsafe { nil }
}

// sign_with_options sign the message with the options. By default, it would precompute
// hash value from message, with recommended_hash function, and then sign the hash value.
pub fn (pv PrivateKey) sign_with_options(message []u8, opts SignerOpts) ![]u8 {
// we're working on mutable copy of SignerOpts, with some issues when make it as a mutable.
// ie, declaring a mutable parameter that accepts a struct with the `@[params]` attribute is not allowed.
mut cfg := opts
match cfg.hash_config {
.with_recomended_hash {
h := pv.recommended_hash()!
match h {
.sha256 {
digest := sha256.sum256(message)
return pv.sign(digest)!
}
.sha384 {
digest := sha512.sum384(message)
return pv.sign(digest)!
}
.sha512 {
digest := sha512.sum512(message)
return pv.sign(digest)!
}
else {
return error('Unsupported hash')
}
}
}
.with_no_hash {
return pv.sign(message)!
}
.with_custom_hash {
if !cfg.allow_custom_hash {
return error('custom hash was not allowed, set it into true')
}
if cfg.custom_hash == unsafe { nil } {
return error('Custom hasher was not defined')
}
// check key size bits
group := voidptr(C.EC_KEY_get0_group(pv.key))
if group == 0 {
return error('fail to load group')
}
num_bits := C.EC_GROUP_get_degree(group)
// check for key size matching
key_size := (num_bits + 7) / 8
// If current Private Key size is bigger then current hash output size,
// by default its not allowed, until set the allow_smaller_size into true
if key_size > cfg.custom_hash.size() {
if !cfg.allow_smaller_size {
return error('Hash into smaller size than current key size was not allowed')
}
}
// otherwise, just hash the message and sign
digest := cfg.custom_hash.sum(message)
defer { unsafe { cfg.custom_hash.free() } }
return pv.sign(digest)!
}
}
return error('Not should be here')
}

// Clear allocated memory for key
Expand Down
Loading
Loading