From c2b7dbf9b41e40614a75cdffbb934bdb34b9543c Mon Sep 17 00:00:00 2001 From: blackshirt Date: Sun, 19 Jan 2025 01:07:19 +0700 Subject: [PATCH] crypto.ecdsa: improve safety checking, unify signing (and verifying) api to accept options (#23463) --- cmd/tools/modules/testing/common.v | 3 + vlib/crypto/ecdsa/README.md | 33 +- vlib/crypto/ecdsa/ecdsa.v | 353 +++++++++++++++----- vlib/crypto/ecdsa/ecdsa_test.v | 87 ++++- vlib/crypto/ecdsa/example/ecdsa_seed_test.v | 83 +++++ vlib/crypto/ecdsa/example/example1.v | 16 + vlib/crypto/ecdsa/util.v | 192 ++++++++++- vlib/crypto/ecdsa/util_test.v | 86 ++++- 8 files changed, 748 insertions(+), 105 deletions(-) create mode 100644 vlib/crypto/ecdsa/example/ecdsa_seed_test.v create mode 100644 vlib/crypto/ecdsa/example/example1.v diff --git a/cmd/tools/modules/testing/common.v b/cmd/tools/modules/testing/common.v index e1db8605399a97..6a5422bbae3684 100644 --- a/cmd/tools/modules/testing/common.v +++ b/cmd/tools/modules/testing/common.v @@ -252,6 +252,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession { 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 + skip_files << 'vlib/crypto/ecdsa/example/ecdsa_seed_test.v' // requires OpenSSL $if tinyc { skip_files << 'examples/database/orm.v' // try fix it } @@ -284,6 +285,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession { skip_files << 'vlib/net/openssl/openssl_compiles_test.c.v' 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/x/ttf/ttf_test.v' skip_files << 'vlib/encoding/iconv/iconv_test.v' // needs libiconv to be installed } @@ -291,6 +293,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession { skip_files << 'vlib/net/openssl/openssl_compiles_test.c.v' 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 // 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/README.md b/vlib/crypto/ecdsa/README.md index 870c6d79992dc9..14cf0d78516b4e 100644 --- a/vlib/crypto/ecdsa/README.md +++ b/vlib/crypto/ecdsa/README.md @@ -2,7 +2,38 @@ `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 \ No newline at end of file +- A famous Bitcoin curve, commonly referred as secp256k1 + +> [!CAUTION] +> This module using low level OpenSSL opaque methods that mostly has been deprecated +> in OpenSSL 3.0. +> Please be aware, likely it would not compile with `-cstrict` options until +> its migrated into supported higher level API. + + +# Example +```v +import crypto.ecdsa + +fn main() { + // create default NIST P-256 secp256r1 curve key pair. If you wish to generate another curve, + // use: `pbkey, pvkey := ecdsa.generate_key(nid: .secp521r1)!` instead. + pbkey, pvkey := ecdsa.generate_key()! + + message_tobe_signed := 'Hello ecdsa'.bytes() + // create a signature with the recommended hash + signature := pvkey.sign(message_tobe_signed)! + + // verify the message with the signature + verified := pbkey.verify(message_tobe_signed, signature)! + dump(verified) // should be true + + // free allocated keys when you have done with your work. + pbkey.free() + pvkey.free() +} +``` \ No newline at end of file diff --git a/vlib/crypto/ecdsa/ecdsa.v b/vlib/crypto/ecdsa/ecdsa.v index 32b7c9e0256b9c..93855e3bc4b7bf 100644 --- a/vlib/crypto/ecdsa/ecdsa.v +++ b/vlib/crypto/ecdsa/ecdsa.v @@ -8,6 +8,10 @@ import crypto import crypto.sha256 import crypto.sha512 +// See https://docs.openssl.org/master/man7/openssl_user_macros/#description +// 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 -I/usr/include/openssl @@ -21,31 +25,32 @@ import crypto.sha512 // C function declarations fn C.EC_KEY_new_by_curve_name(nid int) &C.EC_KEY +fn C.EC_KEY_dup(src &C.EC_KEY) &C.EC_KEY fn C.EC_KEY_generate_key(key &C.EC_KEY) int fn C.EC_KEY_free(key &C.EC_KEY) -fn C.BN_bin2bn(s &u8, len int, ret &C.BIGNUM) &C.BIGNUM +fn C.EC_KEY_set_public_key(key &C.EC_KEY, &C.EC_POINT) int fn C.EC_KEY_set_private_key(key &C.EC_KEY, prv &C.BIGNUM) int fn C.EC_KEY_get0_group(key &C.EC_KEY) &C.EC_GROUP +fn C.EC_KEY_get0_private_key(key &C.EC_KEY) &C.BIGNUM +fn C.EC_KEY_get0_public_key(key &C.EC_KEY) &C.EC_POINT +fn C.EC_KEY_check_key(key &C.EC_KEY) int +fn C.EC_KEY_up_ref(key &C.EC_KEY) int fn C.EC_POINT_new(group &C.EC_GROUP) &C.EC_POINT fn C.EC_POINT_mul(group &C.EC_GROUP, r &C.EC_POINT, n &C.BIGNUM, q &C.EC_POINT, m &C.BIGNUM, ctx &C.BN_CTX) int -fn C.EC_KEY_set_public_key(key &C.EC_KEY, &C.EC_POINT) int +fn C.EC_POINT_cmp(group &C.EC_GROUP, a &C.EC_POINT, b &C.EC_POINT, ctx &C.BN_CTX) int fn C.EC_POINT_free(point &C.EC_POINT) -fn C.BN_free(a &C.BIGNUM) -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 -fn C.EC_KEY_get0_private_key(key &C.EC_KEY) &C.BIGNUM +fn C.EC_GROUP_cmp(a &C.EC_GROUP, b &C.EC_GROUP, ctx &C.BN_CTX) int fn C.BN_num_bits(a &C.BIGNUM) int fn C.BN_bn2bin(a &C.BIGNUM, to &u8) int -fn C.EC_KEY_up_ref(key &C.EC_KEY) int +fn C.BN_bn2binpad(a &C.BIGNUM, to &u8, tolen int) int fn C.BN_cmp(a &C.BIGNUM, b &C.BIGNUM) int -fn C.EC_KEY_get0_public_key(key &C.EC_KEY) &C.EC_POINT -fn C.EC_POINT_cmp(group &C.EC_GROUP, a &C.EC_POINT, b &C.EC_POINT, ctx &C.BN_CTX) int 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 +fn C.BN_bin2bn(s &u8, len int, ret &C.BIGNUM) &C.BIGNUM +fn C.BN_free(a &C.BIGNUM) +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 // NID constants // @@ -74,7 +79,12 @@ pub enum Nid { @[params] pub struct CurveOptions { pub mut: - nid Nid = .prime256v1 // default to NIST P-256 curve + // default to NIST P-256 curve + nid Nid = .prime256v1 + // by default, allow arbitrary size of seed bytes as key. + // Set it to `true` when you need fixed size, using the curve key size. + // Its main purposes is to support the `.new_key_from_seed` call. + fixed_size bool } @[typedef] @@ -95,42 +105,155 @@ struct C.ECDSA_SIG {} @[typedef] struct C.BN_CTX {} +// enum flag to allow flexible PrivateKey size +enum KeyFlag { + // flexible flag to allow flexible-size of seed bytes + flexible + // fixed flag for using underlying curve key size + fixed +} + +// PrivateKey represents ECDSA private key. Actually its a key pair, +// contains private key and public key parts. pub struct PrivateKey { key &C.EC_KEY +mut: + // ks_flag with .flexible value allowing + // flexible-size seed bytes as key. + // When it is `.fixed`, it will use the underlying key size. + ks_flag KeyFlag = .flexible + // ks_size stores size of the seed bytes when ks_flag was .flexible. + // You should set it to a non zero value + ks_size int } +// PublicKey represents ECDSA public key for verifying message. pub struct PublicKey { key &C.EC_KEY } -// Generate a new key pair. If opt was not provided, its default to prime256v1 curve. -pub fn generate_key(opt CurveOptions) !(PublicKey, PrivateKey) { +// PrivateKey.new creates a new key pair. By default, it would create a prime256v1 based key. +pub fn PrivateKey.new(opt CurveOptions) !PrivateKey { + // creates new empty key ec_key := new_curve(opt) if ec_key == 0 { + C.EC_KEY_free(ec_key) return error('Failed to create new EC_KEY') } + // Generates new public and private key for the supplied ec_key object. res := C.EC_KEY_generate_key(ec_key) if res != 1 { C.EC_KEY_free(ec_key) return error('Failed to generate EC_KEY') } + // performs explicit check + chk := C.EC_KEY_check_key(ec_key) + if chk == 0 { + C.EC_KEY_free(ec_key) + return error('EC_KEY_check_key failed') + } + // when using default EC_KEY_generate_key, its using underlying curve key size + // and discarded opt.fixed_size flag when its not set. + priv_key := PrivateKey{ + key: ec_key + ks_flag: .fixed + } + return priv_key +} +// generate_key generates a new key pair. If opt was not provided, its default to prime256v1 curve. +// If you want another curve, use in the following manner: `pubkey, pivkey := ecdsa.generate_key(nid: .secp384r1)!` +pub fn generate_key(opt CurveOptions) !(PublicKey, PrivateKey) { + // creates new empty key + ec_key := new_curve(opt) + if ec_key == 0 { + C.EC_KEY_free(ec_key) + return error('Failed to create new EC_KEY') + } + // we duplicate the empty ec_key and shares similiar curve infos + // and used this as public key + pbkey := C.EC_KEY_dup(ec_key) + if pbkey == 0 { + C.EC_KEY_free(ec_key) + C.EC_KEY_free(pbkey) + return error('Failed on EC_KEY_dup') + } + res := C.EC_KEY_generate_key(ec_key) + if res != 1 { + C.EC_KEY_free(ec_key) + C.EC_KEY_free(pbkey) + return error('Failed to generate EC_KEY') + } + // we take public key bits from above generated key + // and stored in duplicated public key object before. + pubkey_point := voidptr(C.EC_KEY_get0_public_key(ec_key)) + if pubkey_point == 0 { + C.EC_POINT_free(pubkey_point) + C.EC_KEY_free(ec_key) + C.EC_KEY_free(pbkey) + return error('Failed to get public key BIGNUM') + } + np := C.EC_KEY_set_public_key(pbkey, pubkey_point) + if np != 1 { + C.EC_POINT_free(pubkey_point) + C.EC_KEY_free(ec_key) + C.EC_KEY_free(pbkey) + return error('Failed to set public key') + } + // when using default generate_key, its using underlying curve key size + // and discarded opt.fixed_size flag when its not set. priv_key := PrivateKey{ - key: ec_key + key: ec_key + ks_flag: .fixed } pub_key := PublicKey{ - key: ec_key + key: pbkey } return pub_key, priv_key } -// Create a new private key from a seed. If opt was not provided, its default to prime256v1 curve. +// new_key_from_seed creates a new private key from the seed bytes. If opt was not provided, +// its default to prime256v1 curve. +// +// Notes on the seed: +// You should make sure, the seed bytes come from a cryptographically secure random generator, +// likes the `crypto.rand` or other trusted sources. +// Internally, the seed size's would be checked to not exceed the key size of underlying curve, +// ie, 32 bytes length for p-256 and secp256k1, 48 bytes length for p-384 and 64 bytes length for p-521. +// Its recommended to use seed with bytes length matching with underlying curve key size. pub fn new_key_from_seed(seed []u8, opt CurveOptions) !PrivateKey { + // Early exit check + if seed.len == 0 { + return error('Seed with null-length was not allowed') + } // Create a new EC_KEY object with the specified curve ec_key := new_curve(opt) if ec_key == 0 { + C.EC_KEY_free(ec_key) return error('Failed to create new EC_KEY') } + // Retrieve the EC_GROUP object associated with the EC_KEY + // Note: cast with voidptr() to allow -cstrict checks to pass + group := voidptr(C.EC_KEY_get0_group(ec_key)) + if group == 0 { + C.EC_KEY_free(ec_key) + return error('Unable to load group') + } + // Adds early check for upper size, so, we dont hit unnecessary + // call to math intensive calculation, conversion and checking routines. + num_bits := C.EC_GROUP_get_degree(group) + key_size := (num_bits + 7) / 8 + if seed.len > key_size { + C.EC_KEY_free(ec_key) + return error('Seed length exceeds key size') + } + // Check if its using fixed key size or flexible one + if opt.fixed_size { + if seed.len != key_size { + C.EC_KEY_free(ec_key) + return error('seed size doesnt match with curve key size') + } + } // Convert the seed bytes into a BIGNUM bn := C.BN_bin2bn(seed.data, seed.len, 0) if bn == 0 { @@ -146,17 +269,6 @@ pub fn new_key_from_seed(seed []u8, opt CurveOptions) !PrivateKey { } // Now compute the public key // - // Retrieve the EC_GROUP object associated with the 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 @@ -195,14 +307,38 @@ pub fn new_key_from_seed(seed []u8, opt CurveOptions) !PrivateKey { } C.EC_POINT_free(pub_key_point) C.BN_free(bn) - return PrivateKey{ + + mut pvkey := PrivateKey{ key: ec_key } + // we set the flag information on the key + if opt.fixed_size { + // using fixed one + pvkey.ks_flag = .fixed + pvkey.ks_size = key_size + } else { + pvkey.ks_size = seed.len + } + + return pvkey +} + +// 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)! +} + +// sign_with_options signs message with the options. It will be deprecated, +// Use `PrivateKey.sign()` instead. +@[deprecated: 'use PrivateKey.sign() instead'] +pub fn (pv PrivateKey) sign_with_options(message []u8, opt SignerOpts) ![]u8 { + return pv.sign(message, opt) } -// Sign a message with private key -// FIXME: should the message should be hashed? -pub fn (priv_key PrivateKey) sign(message []u8) ![]u8 { +// 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') } @@ -219,49 +355,95 @@ pub fn (priv_key PrivateKey) sign(message []u8) ![]u8 { return signed_data.clone() } -// Verify a signature with public key -pub fn (pub_key PublicKey) verify(message []u8, sig []u8) !bool { - res := C.ECDSA_verify(0, message.data, message.len, sig.data, sig.len, pub_key.key) +// 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) if res == -1 { return error('Failed to verify signature') } return res == 1 } -// Get the seed (private key bytes) -pub fn (priv_key PrivateKey) seed() ![]u8 { +// bytes represent private key as bytes. +pub fn (priv_key PrivateKey) bytes() ![]u8 { bn := voidptr(C.EC_KEY_get0_private_key(priv_key.key)) if bn == 0 { return error('Failed to get private key BIGNUM') } num_bytes := (C.BN_num_bits(bn) + 7) / 8 - mut buf := []u8{len: int(num_bytes)} - res := C.BN_bn2bin(bn, buf.data) + // Get the buffer size to store the seed. + size := if priv_key.ks_flag == .flexible { + // should be non zero + priv_key.ks_size + } else { + num_bytes + } + mut buf := []u8{len: int(size)} + res := C.BN_bn2binpad(bn, buf.data, size) if res == 0 { return error('Failed to convert BIGNUM to bytes') } return buf } -// Get the public key from private key +// seed gets the seed (private key bytes). It will be deprecated. +// Use `PrivateKey.bytes()` instead. +@[deprecated: 'use PrivateKey.bytes() instead'] +pub fn (priv_key PrivateKey) seed() ![]u8 { + return priv_key.bytes() +} + +// Get the public key from private key. pub fn (priv_key PrivateKey) public_key() !PublicKey { - // Increase reference count - res := C.EC_KEY_up_ref(priv_key.key) - if res != 1 { - return error('Failed to increment EC_KEY reference count') + // There are some issues concerned when returning PublicKey directly using underlying + // `PrivateKey.key`. This private key containing sensitive information inside it, so return + // this without care maybe can lead to some serious security impact. + // See https://discord.com/channels/592103645835821068/592320321995014154/1329261267965448253 + // So, we instead return a new EC_KEY opaque based information availables on private key object + // without private key bits has been set on this new opaque. + group := voidptr(C.EC_KEY_get0_group(priv_key.key)) + if group == 0 { + return error('Failed to load group from priv_key') + } + nid := C.EC_GROUP_get_curve_name(group) + if nid != nid_prime256v1 && nid != nid_secp384r1 && nid != nid_secp521r1 && nid != nid_secp256k1 { + return error('Get unsupported curve nid') + } + // get public key point from private key opaque + pubkey_point := voidptr(C.EC_KEY_get0_public_key(priv_key.key)) + if pubkey_point == 0 { + // C.EC_POINT_free(pubkey_point) + // todo: maybe its not set, just calculates new one + return error('Failed to get public key BIGNUM') + } + // creates a new EC_KEY opaque based on the same NID with private key and + // sets public key on it. + pub_key := C.EC_KEY_new_by_curve_name(nid) + np := C.EC_KEY_set_public_key(pub_key, pubkey_point) + if np != 1 { + // C.EC_POINT_free(pubkey_point) + C.EC_KEY_free(pub_key) + return error('Failed to set public key') + } + // performs explicit check + chk := C.EC_KEY_check_key(pub_key) + if chk == 0 { + C.EC_KEY_free(pub_key) + return error('EC_KEY_check_key failed') } + // OK ? return PublicKey{ - key: priv_key.key + key: pub_key } } -// 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 +// +// - 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 { group1 := voidptr(C.EC_KEY_get0_group(priv_key.key)) group2 := voidptr(C.EC_KEY_get0_group(other.key)) @@ -335,10 +517,10 @@ fn new_curve(opt CurveOptions) &C.EC_KEY { 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)) +// Gets recommended hash function of the key. +// Its purposes for hashing message to be signed. +fn recommended_hash(key &C.EC_KEY) !crypto.Hash { + group := voidptr(C.EC_KEY_get0_group(key)) if group == 0 { return error('Unable to load group') } @@ -363,7 +545,7 @@ fn (pv PrivateKey) recommended_hash() !crypto.Hash { } pub enum HashConfig { - with_recomended_hash + with_recommended_hash with_no_hash with_custom_hash } @@ -371,44 +553,46 @@ pub enum HashConfig { @[params] pub struct SignerOpts { pub mut: - hash_config HashConfig = .with_recomended_hash - // make sense when HashConfig != with_recomended_hash + // default to .with_recommended_hash + hash_config HashConfig = .with_recommended_hash + // make sense when HashConfig != with_recommended_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 { +// calc_digest tries to calculates digest (hash) of the message based on options provided. +// If the options was with_no_hash, its return default message without hashing. +fn calc_digest(key &C.EC_KEY, message []u8, opt SignerOpts) ![]u8 { + if message.len == 0 { + return error('null-length messages') + } // 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 + mut cfg := opt match cfg.hash_config { - .with_recomended_hash { - h := pv.recommended_hash()! + .with_no_hash { + // return original message + return message + } + .with_recommended_hash { + h := recommended_hash(key)! match h { .sha256 { - digest := sha256.sum256(message) - return pv.sign(digest)! + return sha256.sum256(message) } .sha384 { - digest := sha512.sum384(message) - return pv.sign(digest)! + return sha512.sum384(message) } .sha512 { - digest := sha512.sum512(message) - return pv.sign(digest)! + return sha512.sum512(message) } 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') @@ -417,7 +601,7 @@ pub fn (pv PrivateKey) sign_with_options(message []u8, opts SignerOpts) ![]u8 { return error('Custom hasher was not defined') } // check key size bits - group := voidptr(C.EC_KEY_get0_group(pv.key)) + group := voidptr(C.EC_KEY_get0_group(key)) if group == 0 { return error('fail to load group') } @@ -431,16 +615,27 @@ pub fn (pv PrivateKey) sign_with_options(message []u8, opts SignerOpts) ![]u8 { 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 digest } } return error('Not should be here') } // Clear allocated memory for key -pub fn key_free(ec_key &C.EC_KEY) { +fn key_free(ec_key &C.EC_KEY) { C.EC_KEY_free(ec_key) } + +// free clears out allocated memory for PublicKey. +// Dont use PublicKey after calling `.free()` +pub fn (pb &PublicKey) free() { + C.EC_KEY_free(pb.key) +} + +// free clears out allocated memory for PrivateKey +// Dont use PrivateKey after calling `.free()` +pub fn (pv &PrivateKey) free() { + C.EC_KEY_free(pv.key) +} diff --git a/vlib/crypto/ecdsa/ecdsa_test.v b/vlib/crypto/ecdsa/ecdsa_test.v index cd8bb4c301e382..caeeaca185bd8e 100644 --- a/vlib/crypto/ecdsa/ecdsa_test.v +++ b/vlib/crypto/ecdsa/ecdsa_test.v @@ -13,25 +13,22 @@ fn test_ecdsa() { println('Signature valid: ${is_valid}') assert is_valid key_free(priv_key.key) + key_free(pub_key.key) } -// This test should exactly has the same behaviour with default sign(message), -// because we passed .with_no_hash flag as an option. -fn test_ecdsa_signing_with_options() { +fn test_ecdsa_signing_with_recommended_hash_options() { // Generate key pair pub_key, priv_key := generate_key() or { panic(err) } // Sign a message message := 'Hello, ECDSA!'.bytes() - opts := SignerOpts{ - hash_config: .with_no_hash - } - signature := priv_key.sign_with_options(message, opts) or { panic(err) } + signature := priv_key.sign(message) or { panic(err) } // Verify the signature is_valid := pub_key.verify(message, signature) or { panic(err) } println('Signature valid: ${is_valid}') key_free(pub_key.key) + key_free(priv_key.key) assert is_valid } @@ -41,6 +38,7 @@ fn test_generate_key() ! { assert pub_key.key != unsafe { nil } assert priv_key.key != unsafe { nil } key_free(priv_key.key) + key_free(pub_key.key) } fn test_new_key_from_seed() ! { @@ -52,6 +50,15 @@ fn test_new_key_from_seed() ! { key_free(priv_key.key) } +fn test_new_key_from_seed_with_leading_zeros_bytes() ! { + // Test generating a key from a seed + seed := [u8(0), u8(1), 2, 3, 4, 5] + priv_key := new_key_from_seed(seed) or { panic(err) } + retrieved_seed := priv_key.seed() or { panic(err) } + assert seed == retrieved_seed + key_free(priv_key.key) +} + fn test_sign_and_verify() ! { // Test signing and verifying a message pub_key, priv_key := generate_key() or { panic(err) } @@ -60,22 +67,26 @@ fn test_sign_and_verify() ! { is_valid := pub_key.verify(message, signature) or { panic(err) } assert is_valid key_free(priv_key.key) + key_free(pub_key.key) } fn test_seed() ! { // Test retrieving the seed from a private key - _, priv_key := generate_key() or { panic(err) } + pub_key, priv_key := generate_key() or { panic(err) } seed := priv_key.seed() or { panic(err) } assert seed.len > 0 key_free(priv_key.key) + key_free(pub_key.key) } fn test_public_key() ! { // Test getting the public key from a private key - _, priv_key := generate_key() or { panic(err) } + pubkk, priv_key := generate_key() or { panic(err) } pub_key1 := priv_key.public_key() or { panic(err) } - pub_key2, _ := generate_key() or { panic(err) } + pub_key2, privkk := generate_key() or { panic(err) } assert !pub_key1.equal(pub_key2) + key_free(pubkk.key) + key_free(privkk.key) key_free(priv_key.key) key_free(pub_key1.key) key_free(pub_key2.key) @@ -83,31 +94,34 @@ fn test_public_key() ! { fn test_private_key_equal() ! { // Test private key equality - _, priv_key1 := generate_key() or { panic(err) } + pbk, priv_key1 := generate_key() or { panic(err) } seed := priv_key1.seed() or { panic(err) } priv_key2 := new_key_from_seed(seed) or { panic(err) } assert priv_key1.equal(priv_key2) + key_free(pbk.key) key_free(priv_key1.key) key_free(priv_key2.key) } fn test_private_key_equality_on_different_curve() ! { // default group - _, priv_key1 := generate_key() or { panic(err) } + pbk, priv_key1 := generate_key() or { panic(err) } seed := priv_key1.seed() or { panic(err) } // using different group priv_key2 := new_key_from_seed(seed, nid: .secp384r1) or { panic(err) } assert !priv_key1.equal(priv_key2) + key_free(pbk.key) key_free(priv_key1.key) key_free(priv_key2.key) } fn test_public_key_equal() ! { // Test public key equality - _, priv_key := generate_key() or { panic(err) } + pbk, priv_key := generate_key() or { panic(err) } pub_key1 := priv_key.public_key() or { panic(err) } pub_key2 := priv_key.public_key() or { panic(err) } assert pub_key1.equal(pub_key2) + key_free(pbk.key) key_free(priv_key.key) key_free(pub_key1.key) key_free(pub_key2.key) @@ -128,24 +142,65 @@ fn test_sign_with_new_key_from_seed() ! { fn test_invalid_signature() ! { // Test verifying an invalid signature - pub_key, _ := generate_key() or { panic(err) } + pub_key, pvk := generate_key() or { panic(err) } message := 'Test message'.bytes() invalid_signature := [u8(1), 2, 3] // Deliberately invalid result := pub_key.verify(message, invalid_signature) or { // Expecting verification to fail assert err.msg() == 'Failed to verify signature' key_free(pub_key.key) + key_free(pvk.key) return } assert !result key_free(pub_key.key) + key_free(pvk.key) } fn test_different_keys_not_equal() ! { // Test that different keys are not equal - _, priv_key1 := generate_key() or { panic(err) } - _, priv_key2 := generate_key() or { panic(err) } + pbk1, priv_key1 := generate_key() or { panic(err) } + pbk2, priv_key2 := generate_key() or { panic(err) } assert !priv_key1.equal(priv_key2) + key_free(pbk1.key) + key_free(pbk2.key) key_free(priv_key1.key) key_free(priv_key2.key) } + +fn test_private_key_new() ! { + priv_key := PrivateKey.new()! + assert priv_key.ks_flag == .fixed + size := ec_key_size(priv_key.key)! + assert size == 32 + pubkey := priv_key.public_key()! + + message := 'Another test message'.bytes() + signature := priv_key.sign(message)! + is_valid := pubkey.verify(message, signature)! + assert is_valid + + // new private key + seed := priv_key.seed()! + priv_key2 := new_key_from_seed(seed)! + pubkey2 := priv_key2.public_key()! + assert priv_key.equal(priv_key2) + assert pubkey.equal(pubkey2) + is_valid2 := pubkey2.verify(message, signature)! + assert is_valid2 + + // generates new key with different curve + priv_key3 := new_key_from_seed(seed, nid: .secp384r1)! + pubkey3 := priv_key3.public_key()! + assert !priv_key3.equal(priv_key2) + assert !pubkey3.equal(pubkey2) + is_valid3 := pubkey3.verify(message, signature)! + assert !is_valid3 + + priv_key.free() + priv_key2.free() + priv_key3.free() + pubkey.free() + pubkey2.free() + pubkey3.free() +} diff --git a/vlib/crypto/ecdsa/example/ecdsa_seed_test.v b/vlib/crypto/ecdsa/example/ecdsa_seed_test.v new file mode 100644 index 00000000000000..314a10fb559e85 --- /dev/null +++ b/vlib/crypto/ecdsa/example/ecdsa_seed_test.v @@ -0,0 +1,83 @@ +import rand +import crypto.ecdsa +import encoding.hex +// The test file placed on its own directory. Its for workaround for +// module lookup problem, because there are two rand module availables, +// between `crypto.rand` and `rand` module. +// See [the talk](https://discord.com/channels/592103645835821068/592294828432424960/1328198034806407311) on discord. + +fn test_new_key_from_seed_with_random_size_and_data() ! { + num_iters := 100 + // default prime256v1 curve key size was 32 bytes. + max_key_size := i32(48) + for i := 0; i <= num_iters; i++ { + m := rand.i32n(max_key_size)! + random_bytes := rand.bytes(m)! + pvkey := ecdsa.new_key_from_seed(random_bytes) or { + // With default size, would error on m > 32 or m == 0 + // dump(m) + if m == 0 { + assert err == error('Seed with null-length was not allowed') + } else if m > 32 { + assert err == error('Seed length exceeds key size') + } else { + assert err == error('EC_KEY_check_key failed') + } + continue + } + ret_seed := pvkey.seed()! + assert random_bytes == ret_seed + pvkey.free() + } +} + +fn test_private_and_public_key_from_string() ! { + // See [this](https://github.com/vlang/v/blob/master/vlib/crypto/ecdsa/util_test.v) for detail + // of material used as a sample. + privkey_sample := '-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAwzj2iiJZaxgk/C6mp +oVskdr6j7akl4bPB8JRnT1J5XNbLPK/iNd/BW+xUJEj/pxWhZANiAAT4/euEWRPV +9cdhtjcKlwF2HrFMLvgxAXFx+01UPfMQ9XOj/85qUhVq1jXraSyDy5FYF28UW4dn +04xVeRuPBbCFxc/uqYj2s5ItHcAZSV3L5sGlXadPfTqoIjCBQAx44k8= +-----END PRIVATE KEY-----' + + pubkey_sample := '-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+P3rhFkT1fXHYbY3CpcBdh6xTC74MQFx +cftNVD3zEPVzo//OalIVatY162ksg8uRWBdvFFuHZ9OMVXkbjwWwhcXP7qmI9rOS +LR3AGUldy+bBpV2nT306qCIwgUAMeOJP +-----END PUBLIC KEY-----' + + // Message tobe signed and verified + message_tobe_signed := 'Example of ECDSA with P-384'.bytes() + // Message signature generated with SHA384 digest with associated key previously. + signature := hex.decode('3066023100b08f6ec77bb319fdb7bce55a2714d7e79cc645d834ee539d8903cfcc88c6fa90df1558856cb840b2dd82e82cd89d7046023100d9d482ca8a6545a3b081fbdd4bb9643a2b4eda4e21fd624833216596032471faae646891f8d2f0bbb86b796c36d3c390')! + + // loads a Privatekey and PublicKey from above sample + privkey := ecdsa.privkey_from_string(privkey_sample)! + pubkey := ecdsa.pubkey_from_string(pubkey_sample)! + // get a public key from private key + pbkey_from_privkey := privkey.public_key()! + + // two public key should be equal, its comes from the same source. + assert pubkey.equal(pbkey_from_privkey) + + // lets create the signature + created_signature := privkey.sign(message_tobe_signed)! + + verified1 := pubkey.verify(message_tobe_signed, signature)! + verified2 := pubkey.verify(message_tobe_signed, created_signature)! + + assert verified1 == true + assert verified2 == true + + // Its also should be verified with pbkey_from_privkey opaque + verified3 := pbkey_from_privkey.verify(message_tobe_signed, signature)! + verified4 := pbkey_from_privkey.verify(message_tobe_signed, created_signature)! + assert verified3 == true + assert verified4 == true + + // release the key + privkey.free() + pubkey.free() + pbkey_from_privkey.free() +} diff --git a/vlib/crypto/ecdsa/example/example1.v b/vlib/crypto/ecdsa/example/example1.v new file mode 100644 index 00000000000000..e3dcfbe4ce3e45 --- /dev/null +++ b/vlib/crypto/ecdsa/example/example1.v @@ -0,0 +1,16 @@ +import crypto.ecdsa + +fn main() { + // create secp256r1, NIST P-256 curve key pair + pbkey, pvkey := ecdsa.generate_key()! + + message_tobe_signed := 'Hello ecdsa'.bytes() + // create signature with recommended hash + signature := pvkey.sign(message_tobe_signed, hash_config: .with_recommended_hash)! + + // verified the message with signature + verified := pbkey.verify(message_tobe_signed, signature, hash_config: .with_recommended_hash)! + dump(verified) // should true + pbkey.free() + pvkey.free() +} diff --git a/vlib/crypto/ecdsa/util.v b/vlib/crypto/ecdsa/util.v index 4a96d0badcf236..0ee1c8f933ee49 100644 --- a/vlib/crypto/ecdsa/util.v +++ b/vlib/crypto/ecdsa/util.v @@ -3,6 +3,8 @@ module ecdsa #include #include #include +#include +#include // #define NID_X9_62_id_ecPublicKey 408 const nid_ec_publickey = C.NID_X9_62_id_ecPublicKey @@ -10,6 +12,12 @@ const nid_ec_publickey = C.NID_X9_62_id_ecPublicKey @[typedef] struct C.EVP_PKEY {} +@[typedef] +struct C.BIO_METHOD {} + +@[typedef] +pub struct C.BIO {} + // EVP_PKEY *EVP_PKEY_new(void); fn C.EVP_PKEY_new() &C.EVP_PKEY @@ -41,6 +49,24 @@ fn C.EVP_PKEY_base_id(key &C.EVP_PKEY) int fn C.EC_GROUP_get_curve_name(g &C.EC_GROUP) int fn C.EC_GROUP_free(group &C.EC_GROUP) +// BIO * BIO_new(BIO_METHOD *type); +fn C.BIO_new(t &C.BIO_METHOD) &C.BIO + +// void BIO_free_all(BIO *a); +fn C.BIO_free_all(a &C.BIO) + +// BIO_METHOD * BIO_s_mem(void); +fn C.BIO_s_mem() &C.BIO_METHOD + +// int BIO_write(BIO *b, const void *buf, int len); +fn C.BIO_write(b &C.BIO, buf &u8, length int) int + +// EVP_PKEY *PEM_read_bio_PrivateKey(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, void *u); +fn C.PEM_read_bio_PrivateKey(bp &C.BIO, x &&C.EVP_PKEY, cb int, u &voidptr) &C.EVP_PKEY + +// EVP_PKEY *PEM_read_bio_PUBKEY(BIO *bp, EVP_PKEY **x, pem_password_cb *cb, void *u); +fn C.PEM_read_bio_PUBKEY(bp &C.BIO, x &&C.EVP_PKEY, cb int, u &voidptr) &C.EVP_PKEY + // pubkey_from_bytes loads ECDSA Public Key from bytes array. // The bytes of data should be a valid of ASN.1 DER serialized SubjectPublicKeyInfo structrue of RFC 5480. // Otherwise, its should an error. @@ -49,6 +75,7 @@ fn C.EC_GROUP_free(group &C.EC_GROUP) // Examples: // ```codeblock // import crypto.pem +// import crypto.ecdsa // // const pubkey_sample = '-----BEGIN PUBLIC KEY----- // MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+P3rhFkT1fXHYbY3CpcBdh6xTC74MQFx @@ -57,7 +84,7 @@ fn C.EC_GROUP_free(group &C.EC_GROUP) // -----END PUBLIC KEY-----' // // block, _ := pem.decode(pubkey_sample) or { panic(err) } -// pubkey := pubkey_from_bytes(block.data)! +// pubkey := ecdsa.pubkey_from_bytes(block.data)! // ``` pub fn pubkey_from_bytes(bytes []u8) !PublicKey { if bytes.len == 0 { @@ -79,7 +106,7 @@ pub fn pubkey_from_bytes(bytes []u8) !PublicKey { eckey := C.EVP_PKEY_get1_EC_KEY(pub_key) if eckey == 0 { - key_free(eckey) + C.EC_KEY_free(eckey) return error('Failed to get ec key') } // check the group for the supported curve(s) @@ -100,9 +127,10 @@ pub fn pubkey_from_bytes(bytes []u8) !PublicKey { } } -// bytes gets the bytes of public key parts of this keypair. +// bytes gets the bytes of public key. pub fn (pbk PublicKey) bytes() ![]u8 { point := voidptr(C.EC_KEY_get0_public_key(pbk.key)) + // defer { C.EC_POINT_free(point)} if point == 0 { C.EC_POINT_free(point) return error('Failed to get public key BIGNUM') @@ -126,6 +154,7 @@ pub fn (pbk PublicKey) bytes() ![]u8 { mut buf := []u8{len: num_bytes} // Get conversion format. + // // The uncompressed form is indicated by 0x04 and the compressed form is indicated // by either 0x02 or 0x03, hybrid 0x06 // The public key MUST be rejected if any other value is included in the first octet. @@ -134,7 +163,162 @@ pub fn (pbk PublicKey) bytes() ![]u8 { return error('bad conversion format') } n := C.EC_POINT_point2oct(group, point, conv_form, buf.data, buf.len, ctx) - + if n == 0 { + return error('EC_POINT_point2oct failed') + } // returns the clone of the buffer[..n] return buf[..n].clone() } + +// pubkey_from_string loads a PublicKey from valid PEM-formatted string in s. +pub fn pubkey_from_string(s string) !PublicKey { + if s.len == 0 { + return error('Null length string was not allowed') + } + mut evpkey := C.EVP_PKEY_new() + bo := C.BIO_new(C.BIO_s_mem()) + if bo == 0 { + return error('Failed to create BIO_new') + } + n := C.BIO_write(bo, s.str, s.len) + if n <= 0 { + C.BIO_free_all(bo) + return error('BIO_write failed') + } + evpkey = C.PEM_read_bio_PUBKEY(bo, &evpkey, 0, 0) + if evpkey == 0 { + C.BIO_free_all(bo) + C.EVP_PKEY_free(evpkey) + return error('Error loading key') + } + // Get the NID of this key, and check if the key object was + // have the correct NID of ec public key type, ie, NID_X9_62_id_ecPublicKey + nid := C.EVP_PKEY_base_id(evpkey) + if nid != nid_ec_publickey { + C.BIO_free_all(bo) + C.EVP_PKEY_free(evpkey) + return error('Get an nid of non ecPublicKey') + } + // Gets the ec key + eckey := C.EVP_PKEY_get1_EC_KEY(evpkey) + if eckey == 0 { + C.BIO_free_all(bo) + C.EC_KEY_free(eckey) + C.EVP_PKEY_free(evpkey) + return error('Failed to get ec key') + } + // check the group for the supported curve(s) + if !is_valid_supported_group(eckey) { + C.BIO_free_all(bo) + C.EC_KEY_free(eckey) + C.EVP_PKEY_free(evpkey) + return error('Unsupported group') + } + chk := C.EC_KEY_check_key(eckey) + if chk == 0 { + C.EC_KEY_free(eckey) + return error('EC_KEY_check_key failed') + } + C.EVP_PKEY_free(evpkey) + C.BIO_free_all(bo) + // Its OK to return + return PublicKey{ + key: eckey + } +} + +// privkey_from_string loads a PrivateKey from valid PEM-formatted string in s. +// Underlying wrapper support for old secg and pkcs8 private key format, but this was not heavily tested. +// This routine does not support for the pkcs8 EncryptedPrivateKeyInfo format. +// See [usage_test.v](https://github.com/vlang/v/blob/master/vlib/crypto/ecdsa/example/ecdsa_seed_test.v) file +// for example of usage. +pub fn privkey_from_string(s string) !PrivateKey { + if s.len == 0 { + return error('null string was not allowed') + } + mut evpkey := C.EVP_PKEY_new() + bo := C.BIO_new(C.BIO_s_mem()) + if bo == 0 { + return error('Failed to create BIO_new') + } + n := C.BIO_write(bo, s.str, s.len) + if n <= 0 { + C.BIO_free_all(bo) + return error('BIO_write failed') + } + evpkey = C.PEM_read_bio_PrivateKey(bo, &evpkey, 0, 0) + if evpkey == 0 { + C.BIO_free_all(bo) + C.EVP_PKEY_free(evpkey) + return error('Error loading key') + } + + // Get the NID of this key, and check if the key object was + // have the correct NID of ec public key type, ie, NID_X9_62_id_ecPublicKey + nid := C.EVP_PKEY_base_id(evpkey) + if nid != nid_ec_publickey { + C.BIO_free_all(bo) + C.EVP_PKEY_free(evpkey) + return error('Get an nid of non ecPublicKey') + } + + eckey := C.EVP_PKEY_get1_EC_KEY(evpkey) + if eckey == 0 { + C.BIO_free_all(bo) + C.EC_KEY_free(eckey) + C.EVP_PKEY_free(evpkey) + return error('Failed to get ec key') + } + // check the group for the supported curve(s) + if !is_valid_supported_group(eckey) { + C.BIO_free_all(bo) + C.EC_KEY_free(eckey) + C.EVP_PKEY_free(evpkey) + return error('Unsupported group') + } + + chk := C.EC_KEY_check_key(eckey) + if chk == 0 { + C.EC_KEY_free(eckey) + return error('EC_KEY_check_key failed') + } + ksize := ec_key_size(eckey)! + + C.EVP_PKEY_free(evpkey) + C.BIO_free_all(bo) + + // Its OK to return + return PrivateKey{ + key: eckey + ks_flag: .fixed + ks_size: ksize + } +} + +// Helpers +// +// is_valid_supported_group checks whether this eckey has valid group of supported curve. +@[inline] +fn is_valid_supported_group(eckey &C.EC_KEY) bool { + group := voidptr(C.EC_KEY_get0_group(eckey)) + if group == 0 { + return false + } + nidgroup := C.EC_GROUP_get_curve_name(group) + if nidgroup == nid_prime256v1 || nidgroup == nid_secp384r1 || nidgroup == nid_secp521r1 + || nidgroup == nid_secp256k1 { + return true + } + return false +} + +// key_size get the key size of this ec key +fn ec_key_size(ec_key &C.EC_KEY) !int { + group := voidptr(C.EC_KEY_get0_group(ec_key)) + if group == 0 { + return error('Unable to load group') + } + num_bits := C.EC_GROUP_get_degree(group) + key_size := (num_bits + 7) / 8 + return key_size +} diff --git a/vlib/crypto/ecdsa/util_test.v b/vlib/crypto/ecdsa/util_test.v index 75eec855f262ff..091a5c5a87e61a 100644 --- a/vlib/crypto/ecdsa/util_test.v +++ b/vlib/crypto/ecdsa/util_test.v @@ -2,7 +2,6 @@ module ecdsa import encoding.hex import crypto.pem -import crypto.sha512 // This material wss generated with https://emn178.github.io/online-tools/ecdsa/key-generator // with curve SECG secp384r1 aka NIST P-384 @@ -20,7 +19,7 @@ LR3AGUldy+bBpV2nT306qCIwgUAMeOJP -----END PUBLIC KEY-----' // Message tobe signed and verified -const message_tobe_signed = 'Example of ECDSA with P-384' +const message_tobe_signed = 'Example of ECDSA with P-384'.bytes() // Message signature created with SHA384 digest with associated above key const expected_signature = hex.decode('3066023100b08f6ec77bb319fdb7bce55a2714d7e79cc645d834ee539d8903cfcc88c6fa90df1558856cb840b2dd82e82cd89d7046023100d9d482ca8a6545a3b081fbdd4bb9643a2b4eda4e21fd624833216596032471faae646891f8d2f0bbb86b796c36d3c390')! @@ -28,11 +27,13 @@ fn test_load_pubkey_from_der_serialized_bytes() ! { block, _ := pem.decode(public_key_sample) or { panic(err) } pbkey := pubkey_from_bytes(block.data)! - status_without_hashed := pbkey.verify(message_tobe_signed.bytes(), expected_signature)! + status_without_hashed := pbkey.verify(message_tobe_signed, expected_signature, + hash_config: .with_no_hash + )! assert status_without_hashed == false - hashed_msg := sha512.sum384(message_tobe_signed.bytes()) - status_with_hashed := pbkey.verify(hashed_msg, expected_signature)! + // expected signature was comes from hashed message with sha384 + status_with_hashed := pbkey.verify(message_tobe_signed, expected_signature)! assert status_with_hashed == true key_free(pbkey.key) } @@ -49,3 +50,78 @@ fn test_for_pubkey_bytes() ! { key_free(pbkey.key) key_free(pvkey.key) } + +// above pem-formatted private key read with +// `$openssl ec -in vlib/crypto/ecdsa/example.pem -text -param_out -check` +// produces following result: +// ```codeblock +// read EC key +// Private-Key: (384 bit) +// priv: +// 30:ce:3d:a2:88:96:5a:c6:09:3f:0b:a9:a9:a1:5b: +// 24:76:be:a3:ed:a9:25:e1:b3:c1:f0:94:67:4f:52: +// 79:5c:d6:cb:3c:af:e2:35:df:c1:5b:ec:54:24:48: +// ff:a7:15 +// pub: +// 04:f8:fd:eb:84:59:13:d5:f5:c7:61:b6:37:0a:97: +// 01:76:1e:b1:4c:2e:f8:31:01:71:71:fb:4d:54:3d: +// f3:10:f5:73:a3:ff:ce:6a:52:15:6a:d6:35:eb:69: +// 2c:83:cb:91:58:17:6f:14:5b:87:67:d3:8c:55:79: +// 1b:8f:05:b0:85:c5:cf:ee:a9:88:f6:b3:92:2d:1d: +// c0:19:49:5d:cb:e6:c1:a5:5d:a7:4f:7d:3a:a8:22: +// 30:81:40:0c:78:e2:4f +// ASN1 OID: secp384r1 +// NIST CURVE: P-384 +// EC Key valid. +// writing EC key +// -----BEGIN EC PRIVATE KEY----- +// MIGkAgEBBDAwzj2iiJZaxgk/C6mpoVskdr6j7akl4bPB8JRnT1J5XNbLPK/iNd/B +// W+xUJEj/pxWgBwYFK4EEACKhZANiAAT4/euEWRPV9cdhtjcKlwF2HrFMLvgxAXFx +// +01UPfMQ9XOj/85qUhVq1jXraSyDy5FYF28UW4dn04xVeRuPBbCFxc/uqYj2s5It +// HcAZSV3L5sGlXadPfTqoIjCBQAx44k8= +// -----END EC PRIVATE KEY----- +// ``` +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 + + // public key part + pbkey := pvkey.public_key()! + pbkey_bytes := pbkey.bytes()! + expected_pubkey_bytes := '04f8fdeb845913d5f5c761b6370a9701761eb14c2ef831017171fb4d543df310f573a3ffce6a52156ad635eb692c83cb9158176f145b8767d38c55791b8f05b085c5cfeea988f6b3922d1dc019495dcbe6c1a55da74f7d3aa8223081400c78e24f' + assert pbkey_bytes.hex() == expected_pubkey_bytes + + // lets sign the message with default hash, ie, sha384 + signature := pvkey.sign(message_tobe_signed)! + + verified := pbkey.verify(message_tobe_signed, signature)! + assert verified == true + pvkey.free() + pbkey.free() +} + +fn test_load_pubkey_from_string_and_used_for_verifying() ! { + pbkey := pubkey_from_string(public_key_sample)! + pbkey_bytes := pbkey.bytes()! + expected_pubkey_bytes := '04f8fdeb845913d5f5c761b6370a9701761eb14c2ef831017171fb4d543df310f573a3ffce6a52156ad635eb692c83cb9158176f145b8767d38c55791b8f05b085c5cfeea988f6b3922d1dc019495dcbe6c1a55da74f7d3aa8223081400c78e24f' + assert pbkey_bytes.hex() == expected_pubkey_bytes + + // expected signature was comes from hashed message with sha384 + status_with_hashed := pbkey.verify(message_tobe_signed, expected_signature)! + assert status_with_hashed == true + pbkey.free() +} + +// test for loading privat key from unsupported curve should fail. +fn test_load_privkey_from_string_with_unsupported_curve() ! { + // generated with openssl ecparam -name secp192k1 -genkey -noout -out key.pem + key := '-----BEGIN EC PRIVATE KEY----- +MFwCAQEEGDHV+WhJL2UjUhgMLh52k0RJjRebtu4HvqAHBgUrgQQAH6E0AzIABFyF +UHhnmmVRraSwrVkPdYIeXhH/Ob4+8OLcwrQBMv4RXsD1GVFsgkvEYDTEb/vnMA== +-----END EC PRIVATE KEY-----' + _ := privkey_from_string(key) or { + assert err == error('Unsupported group') + return + } +}