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: migrate core routines for signing (and verifying) #23705

Merged
merged 5 commits into from
Feb 17, 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
3 changes: 3 additions & 0 deletions cmd/tools/modules/testing/common.v
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/util_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/example/ecdsa_seed_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/example/ensure_compatibility_with_net_openssl_test.v' // requires OpenSSL
$if tinyc {
skip_files << 'examples/database/orm.v' // try fix it
}
Expand Down Expand Up @@ -285,6 +286,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/util_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/example/ecdsa_seed_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/example/ensure_compatibility_with_net_openssl_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
}
Expand All @@ -293,6 +295,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
skip_files << 'vlib/crypto/ecdsa/ecdsa_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/util_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/example/ecdsa_seed_test.v' // requires OpenSSL
skip_files << 'vlib/crypto/ecdsa/example/ensure_compatibility_with_net_openssl_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
54 changes: 50 additions & 4 deletions vlib/crypto/ecdsa/ecdsa.c.v
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,20 @@ module ecdsa
// should be 0x30000000L, but a lot of EC_KEY method was deprecated on version 3.0
// #define OPENSSL_API_COMPAT 0x10100000L

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

#flag linux -I/usr/local/include/openssl
#flag linux -L/usr/local/lib64/

#flag -I/usr/include/openssl

#flag -lcrypto
#flag darwin -I/usr/local/opt/openssl/include
#flag darwin -L/usr/local/opt/openssl/lib

#include <openssl/ecdsa.h>
#include <openssl/obj_mac.h>
#include <openssl/objects.h>
#include <openssl/bn.h>
#include <openssl/evp.h>
#include <openssl/ec.h>
Expand All @@ -34,11 +39,37 @@ fn C.EVP_PKEY_new() &C.EVP_PKEY
fn C.EVP_PKEY_free(key &C.EVP_PKEY)
fn C.EVP_PKEY_get1_EC_KEY(pkey &C.EVP_PKEY) &C.EC_KEY
fn C.EVP_PKEY_base_id(key &C.EVP_PKEY) int
fn C.EVP_PKEY_get_bits(pkey &C.EVP_PKEY) int
fn C.EVP_PKEY_size(key &C.EVP_PKEY) int

// no-prehash signing (verifying)
fn C.EVP_PKEY_sign(ctx &C.EVP_PKEY_CTX, sig &u8, siglen &usize, tbs &u8, tbslen int) int
fn C.EVP_PKEY_sign_init(ctx &C.EVP_PKEY_CTX) int
fn C.EVP_PKEY_verify_init(ctx &C.EVP_PKEY_CTX) int
fn C.EVP_PKEY_verify(ctx &C.EVP_PKEY_CTX, sig &u8, siglen int, tbs &u8, tbslen int) int

// single shoot digest signing (verifying) routine
fn C.EVP_DigestSign(ctx &C.EVP_MD_CTX, sig &u8, siglen &usize, tbs &u8, tbslen int) int
fn C.EVP_DigestVerify(ctx &C.EVP_MD_CTX, sig &u8, siglen int, tbs &u8, tbslen int) int

// Message digest routines
fn C.EVP_DigestInit(ctx &C.EVP_MD_CTX, md &C.EVP_MD) int
fn C.EVP_DigestUpdate(ctx &C.EVP_MD_CTX, d voidptr, cnt int) int
fn C.EVP_DigestFinal(ctx &C.EVP_MD_CTX, md &u8, s &usize) int

// Recommended hashed signing/verifying routines
fn C.EVP_DigestSignInit(ctx &C.EVP_MD_CTX, pctx &&C.EVP_PKEY_CTX, tipe &C.EVP_MD, e voidptr, pkey &C.EVP_PKEY) int
fn C.EVP_DigestSignUpdate(ctx &C.EVP_MD_CTX, d voidptr, cnt int) int
fn C.EVP_DigestSignFinal(ctx &C.EVP_MD_CTX, sig &u8, siglen &usize) int
fn C.EVP_DigestVerifyInit(ctx &C.EVP_MD_CTX, pctx &&C.EVP_PKEY_CTX, tipe &C.EVP_MD, e voidptr, pkey &C.EVP_PKEY) int
fn C.EVP_DigestVerifyUpdate(ctx &C.EVP_MD_CTX, d voidptr, cnt int) int
fn C.EVP_DigestVerifyFinal(ctx &C.EVP_MD_CTX, sig &u8, siglen int) int

// EVP_PKEY Context
@[typedef]
struct C.EVP_PKEY_CTX {}

fn C.EVP_PKEY_CTX_new(pkey &C.EVP_PKEY, e voidptr) &C.EVP_PKEY_CTX
fn C.EVP_PKEY_CTX_new_id(id int, e voidptr) &C.EVP_PKEY_CTX
fn C.EVP_PKEY_keygen_init(ctx &C.EVP_PKEY_CTX) int
fn C.EVP_PKEY_keygen(ctx &C.EVP_PKEY_CTX, ppkey &&C.EVP_PKEY) int
Expand Down Expand Up @@ -124,3 +155,18 @@ struct C.ECDSA_SIG {}
fn C.ECDSA_size(key &C.EC_KEY) u32
fn C.ECDSA_sign(type_ int, dgst &u8, dgstlen int, sig &u8, siglen &u32, eckey &C.EC_KEY) int
fn C.ECDSA_verify(type_ int, dgst &u8, dgstlen int, sig &u8, siglen int, eckey &C.EC_KEY) int

@[typedef]
struct C.EVP_MD_CTX {}

fn C.EVP_MD_CTX_new() &C.EVP_MD_CTX
fn C.EVP_MD_CTX_free(ctx &C.EVP_MD_CTX)

// Wrapper of digest and signing related of the C opaque and functions.
@[typedef]
struct C.EVP_MD {}

fn C.EVP_sha256() &C.EVP_MD
fn C.EVP_sha384() &C.EVP_MD
fn C.EVP_sha512() &C.EVP_MD
fn C.EVP_MD_get_size(md &C.EVP_MD) int // -1 failure
185 changes: 171 additions & 14 deletions vlib/crypto/ecdsa/ecdsa.v
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,12 @@ pub fn PrivateKey.new(opt CurveOptions) !PrivateKey {
// sign performs signing the message with the options. By default options,
// it will perform hashing before signing the message.
pub fn (pv PrivateKey) sign(message []u8, opt SignerOpts) ![]u8 {
digest := calc_digest(pv.key, message, opt)!
return pv.sign_message(digest)!
if pv.evpkey != unsafe { nil } {
digest := calc_digest_with_evpkey(pv.evpkey, message, opt)!
return sign_digest(pv.evpkey, digest)!
}
digest := calc_digest_with_eckey(pv.key, message, opt)!
return pv.sign_digest(digest)!
}

// sign_with_options signs message with the options. It will be deprecated,
Expand All @@ -307,18 +311,18 @@ pub fn (pv PrivateKey) sign_with_options(message []u8, opt SignerOpts) ![]u8 {
return pv.sign(message, opt)
}

// sign_message sign a message with private key.
fn (priv_key PrivateKey) sign_message(message []u8) ![]u8 {
if message.len == 0 {
return error('Message cannot be null or empty')
// sign_digest sign a digest with private key.
fn (pv PrivateKey) sign_digest(digest []u8) ![]u8 {
if digest.len == 0 {
return error('Digest cannot be null or empty')
}
mut sig_len := u32(0)
sig_size := C.ECDSA_size(priv_key.key)
sig_size := C.ECDSA_size(pv.key)
sig := unsafe { malloc(int(sig_size)) }
res := C.ECDSA_sign(0, message.data, message.len, sig, &sig_len, priv_key.key)
res := C.ECDSA_sign(0, digest.data, digest.len, sig, &sig_len, pv.key)
if res != 1 {
unsafe { free(sig) }
return error('Failed to sign message')
return error('Failed to sign digest')
}
signed_data := unsafe { sig.vbytes(int(sig_len)) }
unsafe { free(sig) }
Expand Down Expand Up @@ -469,9 +473,13 @@ pub struct PublicKey {
// verify verifies a message with the signature are valid with public key provided .
// You should provide it with the same SignerOpts used with the `.sign()` call.
// or verify would fail (false).
pub fn (pub_key PublicKey) verify(message []u8, sig []u8, opt SignerOpts) !bool {
digest := calc_digest(pub_key.key, message, opt)!
res := C.ECDSA_verify(0, digest.data, digest.len, sig.data, sig.len, pub_key.key)
pub fn (pb PublicKey) verify(message []u8, sig []u8, opt SignerOpts) !bool {
if pb.evpkey != unsafe { nil } {
digest := calc_digest_with_evpkey(pb.evpkey, message, opt)!
return verify_signature(pb.evpkey, sig, digest)
}
digest := calc_digest_with_eckey(pb.key, message, opt)!
res := C.ECDSA_verify(0, digest.data, digest.len, sig.data, sig.len, pb.key)
if res == -1 {
return error('Failed to verify signature')
}
Expand Down Expand Up @@ -582,9 +590,9 @@ fn calc_digest_with_recommended_hash(key &C.EC_KEY, msg []u8) ![]u8 {
}
}

// calc_digest tries to calculates digest (hash) of the message based on options provided.
// calc_digest_with_eckey tries to calculates digest (hash) of the message based on options provided.
// If the options was .with_no_hash, its has the same behaviour with .with_recommended_hash.
fn calc_digest(key &C.EC_KEY, message []u8, opt SignerOpts) ![]u8 {
fn calc_digest_with_eckey(key &C.EC_KEY, message []u8, opt SignerOpts) ![]u8 {
if message.len == 0 {
return error('null-length messages')
}
Expand Down Expand Up @@ -640,3 +648,152 @@ fn calc_digest(key &C.EC_KEY, message []u8, opt SignerOpts) ![]u8 {
}
return error('Not should be here')
}

// calc_digest_with_evpkey get the digest of the messages under the EVP_PKEY and options
fn calc_digest_with_evpkey(key &C.EVP_PKEY, message []u8, opt SignerOpts) ![]u8 {
if message.len == 0 {
return error('null-length messages')
}
bits_size := C.EVP_PKEY_get_bits(key)
if bits_size <= 0 {
return error(' bits_size was invalid')
}
key_size := (bits_size + 7) / 8

match opt.hash_config {
.with_no_hash, .with_recommended_hash {
md := default_digest(key)!
return calc_digest_with_md(message, md)!
}
.with_custom_hash {
mut cfg := opt
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')
}
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')
}
}
// we need to reset the custom hash before writes message
cfg.custom_hash.reset()
_ := cfg.custom_hash.write(message)!
digest := cfg.custom_hash.sum([]u8{})

return digest
}
}
return error('Not should be here')
}

// sign_digest signs the digest with the key. Under the hood, EVP_PKEY_sign() does not
// hash the data to be signed, and therefore is normally used to sign digests.
fn sign_digest(key &C.EVP_PKEY, digest []u8) ![]u8 {
ctx := C.EVP_PKEY_CTX_new(key, 0)
if ctx == 0 {
C.EVP_PKEY_CTX_free(ctx)
return error('EVP_PKEY_CTX_new failed')
}
sin := C.EVP_PKEY_sign_init(ctx)
if sin != 1 {
C.EVP_PKEY_CTX_free(ctx)
return error('EVP_PKEY_sign_init failed')
}
// siglen was used to store the size of the signature output. When EVP_PKEY_sign
// was called with NULL signature buffer, siglen will tell maximum size of signature.
siglen := usize(C.EVP_PKEY_size(key))
st := C.EVP_PKEY_sign(ctx, 0, &siglen, digest.data, digest.len)
if st <= 0 {
C.EVP_PKEY_CTX_free(ctx)
return error('Get null buffer length on EVP_PKEY_sign')
}
sig := []u8{len: int(siglen)}
do := C.EVP_PKEY_sign(ctx, sig.data, &siglen, digest.data, digest.len)
if do <= 0 {
C.EVP_PKEY_CTX_free(ctx)
return error('EVP_PKEY_sign fails to sign message')
}
// siglen now contains actual length of the signature buffer.
signed := sig[..siglen].clone()

// Cleans up
unsafe { sig.free() }
C.EVP_PKEY_CTX_free(ctx)

return signed
}

// verify_signature verifies the signature for the digest under the provided key.
fn verify_signature(key &C.EVP_PKEY, sig []u8, digest []u8) bool {
ctx := C.EVP_PKEY_CTX_new(key, 0)
if ctx == 0 {
C.EVP_PKEY_CTX_free(ctx)
return false
}
vinit := C.EVP_PKEY_verify_init(ctx)
if vinit != 1 {
C.EVP_PKEY_CTX_free(ctx)
return false
}
res := C.EVP_PKEY_verify(ctx, sig.data, sig.len, digest.data, digest.len)
if res <= 0 {
C.EVP_PKEY_CTX_free(ctx)
return false
}
C.EVP_PKEY_CTX_free(ctx)
return res == 1
}

// calc_digest_with_md get the digest of the msg using md digest algorithm
fn calc_digest_with_md(msg []u8, md &C.EVP_MD) ![]u8 {
ctx := C.EVP_MD_CTX_new()
if ctx == 0 {
C.EVP_MD_CTX_free(ctx)
return error('EVP_MD_CTX_new failed')
}
nt := C.EVP_DigestInit(ctx, md)
assert nt == 1
upd := C.EVP_DigestUpdate(ctx, msg.data, msg.len)
assert upd == 1

size := usize(C.EVP_MD_get_size(md))
out := []u8{len: int(size)}

fin := C.EVP_DigestFinal(ctx, out.data, &size)
assert fin == 1

digest := out[..size].clone()
// cleans up
unsafe { out.free() }
C.EVP_MD_CTX_free(ctx)

return digest
}

// default_digest gets the default digest (hash) algorithm for this key.
fn default_digest(key &C.EVP_PKEY) !&C.EVP_MD {
// get bits size of this key
bits_size := C.EVP_PKEY_get_bits(key)
if bits_size <= 0 {
return error(' this size isnt available.')
}
// based on this bits_size, choose appropriate digest algorithm
match true {
bits_size <= 256 {
return voidptr(C.EVP_sha256())
}
bits_size > 256 && bits_size <= 384 {
return voidptr(C.EVP_sha384())
}
bits_size > 384 {
return voidptr(C.EVP_sha512())
}
else {
return error('Unsupported bits size')
}
}
return error('should not here')
}
2 changes: 1 addition & 1 deletion vlib/crypto/ecdsa/example/ecdsa_seed_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ fn test_new_key_from_seed_with_random_size_and_data() ! {
}
continue
}
ret_seed := pvkey.seed()!
ret_seed := pvkey.bytes()!
assert random_bytes == ret_seed
pvkey.free()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import net.openssl
import crypto.ecdsa

fn test_openssl_and_crypto_ecdsa_are_compatible() {
pbkey, pvkey := ecdsa.generate_key()!
message_tobe_signed := 'Hello ecdsa'.bytes()
signature := pvkey.sign(message_tobe_signed)!
verified := pbkey.verify(message_tobe_signed, signature)!
assert verified
pbkey.free()
pvkey.free()
c := openssl.SSLConn{}
assert c.str().contains('in_memory_verification: false')
}
4 changes: 2 additions & 2 deletions vlib/crypto/ecdsa/util_test.v
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ fn test_for_pubkey_bytes() ! {
pb := '0421af184ac64c8a13e66c65d4f1ad31677edeaa97af791aef73b66ea26d1623a411f67b6c4d842ba22fa39d1216bd64acef00a1b924ac11a10af679ac3a7eb2fd'
pvkey := new_key_from_seed(hex.decode(pv)!)!

assert pvkey.seed()!.hex() == pv
assert pvkey.bytes()!.hex() == pv
pbkey := pvkey.public_key()!
assert pbkey.bytes()!.hex() == pb
pbkey.free()
Expand Down Expand Up @@ -87,7 +87,7 @@ fn test_for_pubkey_bytes() ! {
fn test_load_privkey_from_string_sign_and_verify() ! {
pvkey := privkey_from_string(privatekey_sample)!
expected_pvkey_bytes := '30ce3da288965ac6093f0ba9a9a15b2476bea3eda925e1b3c1f094674f52795cd6cb3cafe235dfc15bec542448ffa715'
assert pvkey.seed()!.hex() == expected_pvkey_bytes
assert pvkey.bytes()!.hex() == expected_pvkey_bytes

// public key part
pbkey := pvkey.public_key()!
Expand Down
Loading