From 66e1d14bb48ee425c70a95dbfd32aff2b40f356e Mon Sep 17 00:00:00 2001 From: blackshirt Date: Mon, 17 Feb 2025 23:49:32 +0700 Subject: [PATCH] crypto.ecdsa: migrate core routines for signing (and verifying), it now requires using OpenSSL 3 (#23705) --- cmd/tools/modules/testing/common.v | 3 + vlib/crypto/ecdsa/ecdsa.c.v | 54 ++++- vlib/crypto/ecdsa/ecdsa.v | 185 ++++++++++++++++-- vlib/crypto/ecdsa/example/ecdsa_seed_test.v | 2 +- ...sure_compatibility_with_net_openssl_test.v | 14 ++ vlib/crypto/ecdsa/util_test.v | 4 +- 6 files changed, 241 insertions(+), 21 deletions(-) create mode 100644 vlib/crypto/ecdsa/example/ensure_compatibility_with_net_openssl_test.v diff --git a/cmd/tools/modules/testing/common.v b/cmd/tools/modules/testing/common.v index b0bddcacf074de..5fbdd06ca07634 100644 --- a/cmd/tools/modules/testing/common.v +++ b/cmd/tools/modules/testing/common.v @@ -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 } @@ -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 } @@ -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' } diff --git a/vlib/crypto/ecdsa/ecdsa.c.v b/vlib/crypto/ecdsa/ecdsa.c.v index 5720322b53963d..26af193ddd1c73 100644 --- a/vlib/crypto/ecdsa/ecdsa.c.v +++ b/vlib/crypto/ecdsa/ecdsa.c.v @@ -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 #include -#include #include #include #include @@ -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 @@ -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 diff --git a/vlib/crypto/ecdsa/ecdsa.v b/vlib/crypto/ecdsa/ecdsa.v index 0fe198e79c115a..96382da6d71cc5 100644 --- a/vlib/crypto/ecdsa/ecdsa.v +++ b/vlib/crypto/ecdsa/ecdsa.v @@ -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, @@ -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) } @@ -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') } @@ -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') } @@ -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') +} diff --git a/vlib/crypto/ecdsa/example/ecdsa_seed_test.v b/vlib/crypto/ecdsa/example/ecdsa_seed_test.v index 314a10fb559e85..72333e04af80df 100644 --- a/vlib/crypto/ecdsa/example/ecdsa_seed_test.v +++ b/vlib/crypto/ecdsa/example/ecdsa_seed_test.v @@ -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() } diff --git a/vlib/crypto/ecdsa/example/ensure_compatibility_with_net_openssl_test.v b/vlib/crypto/ecdsa/example/ensure_compatibility_with_net_openssl_test.v new file mode 100644 index 00000000000000..28ef0870b0172f --- /dev/null +++ b/vlib/crypto/ecdsa/example/ensure_compatibility_with_net_openssl_test.v @@ -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') +} diff --git a/vlib/crypto/ecdsa/util_test.v b/vlib/crypto/ecdsa/util_test.v index b4d7f8ad923fe3..5d67d39e6be288 100644 --- a/vlib/crypto/ecdsa/util_test.v +++ b/vlib/crypto/ecdsa/util_test.v @@ -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() @@ -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()!