From 920688e8d28eb45b4638a1b85bb7b1c784684e05 Mon Sep 17 00:00:00 2001 From: blackshirt Date: Wed, 8 Jan 2025 09:12:38 +0000 Subject: [PATCH] expand ecdsa module to support another curve --- vlib/crypto/ecdsa/README.md | 8 ++ vlib/crypto/ecdsa/ecdsa.v | 222 ++++++++++++++++++++++++++++++--- vlib/crypto/ecdsa/ecdsa_test.v | 30 +++++ vlib/crypto/ecdsa/util.v | 138 ++++++++++++++++++++ vlib/crypto/ecdsa/util_test.v | 37 ++++++ 5 files changed, 417 insertions(+), 18 deletions(-) create mode 100644 vlib/crypto/ecdsa/README.md create mode 100644 vlib/crypto/ecdsa/util.v create mode 100644 vlib/crypto/ecdsa/util_test.v diff --git a/vlib/crypto/ecdsa/README.md b/vlib/crypto/ecdsa/README.md new file mode 100644 index 00000000000000..0a9d049f13b8bf --- /dev/null +++ b/vlib/crypto/ecdsa/README.md @@ -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 \ No newline at end of file diff --git a/vlib/crypto/ecdsa/ecdsa.v b/vlib/crypto/ecdsa/ecdsa.v index ae90980c090c8b..dc11051af7602a 100644 --- a/vlib/crypto/ecdsa/ecdsa.v +++ b/vlib/crypto/ecdsa/ecdsa.v @@ -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 @@ -40,8 +45,34 @@ fn C.BN_CTX_new() &C.BN_CTX fn C.BN_CTX_free(ctx &C.BN_CTX) // 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] +struct CurveOptions { + nid Nid = .prime256v1 // default to NIST P-256 curve +} + @[typedef] struct C.EC_KEY {} @@ -68,10 +99,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') } @@ -89,11 +119,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') } @@ -151,6 +180,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') @@ -204,19 +234,40 @@ 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 := C.EC_KEY_get0_group(priv_key.key) + group2 := 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 := 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 + } + 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 := C.EC_KEY_get0_group(pub_key.key) + group2 := C.EC_KEY_get0_group(other.key) + ctx := C.BN_CTX_new() if ctx == 0 { return false @@ -224,8 +275,143 @@ pub fn (pub_key PublicKey) equal(other PublicKey) bool { 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 := C.EC_KEY_get0_public_key(pub_key.key) + point2 := C.EC_KEY_get0_public_key(other.key) + 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 := 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 { +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 := 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 diff --git a/vlib/crypto/ecdsa/ecdsa_test.v b/vlib/crypto/ecdsa/ecdsa_test.v index 18e3d7ab3cf214..c4c67ecf240bcb 100644 --- a/vlib/crypto/ecdsa/ecdsa_test.v +++ b/vlib/crypto/ecdsa/ecdsa_test.v @@ -15,6 +15,25 @@ fn test_ecdsa() { key_free(priv_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() { + // 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) } + + // Verify the signature + is_valid := pub_key.verify(message, signature) or { panic(err) } + println('Signature valid: ${is_valid}') + assert is_valid +} + fn test_generate_key() ! { // Test key generation pub_key, priv_key := generate_key() or { panic(err) } @@ -71,6 +90,15 @@ fn test_private_key_equal() ! { key_free(priv_key2.key) } +fn test_private_key_equality_on_different_curve() ! { + // default group + _, 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) +} + fn test_public_key_equal() ! { // Test public key equality _, priv_key := generate_key() or { panic(err) } @@ -118,3 +146,5 @@ fn test_different_keys_not_equal() ! { key_free(priv_key1.key) key_free(priv_key2.key) } + +// Example was taken from https://csrc.nist.gov/CSRC/media/Projects/Cryptographic-Standards-and-Guidelines/documents/examples/P384_SHA384.pdf diff --git a/vlib/crypto/ecdsa/util.v b/vlib/crypto/ecdsa/util.v new file mode 100644 index 00000000000000..bcbdcb410d7568 --- /dev/null +++ b/vlib/crypto/ecdsa/util.v @@ -0,0 +1,138 @@ +module ecdsa + +#include +#include + +// #define NID_X9_62_id_ecPublicKey 408 +const nid_ec_publickey = C.NID_X9_62_id_ecPublicKey + +@[typedef] +struct C.EVP_PKEY {} + +// EVP_PKEY *EVP_PKEY_new(void); +fn C.EVP_PKEY_new() &C.EVP_PKEY + +// EVP_PKEY_free(EVP_PKEY *key); +fn C.EVP_PKEY_free(key &C.EVP_PKEY) + +// EC_KEY *EVP_PKEY_get1_EC_KEY(EVP_PKEY *pkey); +fn C.EVP_PKEY_get1_EC_KEY(pkey &C.EVP_PKEY) &C.EC_KEY + +// EVP_PKEY *d2i_PUBKEY(EVP_PKEY **a, const unsigned char **pp, long length); +fn C.d2i_PUBKEY(mut k &C.EVP_PKEY, pp &u8, length u32) &C.EVP_PKEY + +// point_conversion_form_t EC_KEY_get_conv_form(const EC_KEY *key); +fn C.EC_KEY_get_conv_form(k &C.EC_KEY) int + +// EC_GROUP_get_degree +fn C.EC_GROUP_get_degree(g &C.EC_GROUP) int + +// const EC_POINT *EC_KEY_get0_public_key(const EC_KEY *key); +fn C.EC_KEY_get0_public_key(key &C.EC_KEY) &C.EC_POINT + +// size_t EC_POINT_point2oct(const EC_GROUP *group, const EC_POINT *point, point_conversion_form_t form, uint8_t *buf, size_t max_out, BN_CTX *ctx); +fn C.EC_POINT_point2oct(g &C.EC_GROUP, p &C.EC_POINT, form int, buf &u8, max_out int, ctx &C.BN_CTX) int + +// int EVP_PKEY_get_base_id(const EVP_PKEY *pkey); +fn C.EVP_PKEY_base_id(key &C.EVP_PKEY) int + +// int EC_GROUP_get_curve_name(const EC_GROUP *group); +fn C.EC_GROUP_get_curve_name(g &C.EC_GROUP) int +fn C.EC_GROUP_free(group &C.EC_GROUP) + +// 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. +// Typically, you can load the bytes from pem formatted of ecdsa public key. +// +// Examples: +// ```codeblock +// import crypto.pem +// +// const pubkey_sample = '-----BEGIN PUBLIC KEY----- +// MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+P3rhFkT1fXHYbY3CpcBdh6xTC74MQFx +// cftNVD3zEPVzo//OalIVatY162ksg8uRWBdvFFuHZ9OMVXkbjwWwhcXP7qmI9rOS +// LR3AGUldy+bBpV2nT306qCIwgUAMeOJP +// -----END PUBLIC KEY-----' +// +// block, _ := pem.decode(pubkey_sample) or { panic(err) } +// pubkey := pubkey_from_bytes(block.data)! +// ``` +pub fn pubkey_from_bytes(bytes []u8) !PublicKey { + if bytes.len == 0 { + return error('Invalid bytes') + } + mut pub_key := C.EVP_PKEY_new() + pub_key = C.d2i_PUBKEY(mut &pub_key, &bytes.data, bytes.len) + if pub_key == 0 { + C.EVP_PKEY_free(pub_key) + return error('Error loading public key') + } + // Get the NID of this pubkey, and check if the pubkey object was + // have the correct NID of ec public key type, ie, NID_X9_62_id_ecPublicKey + nid := C.EVP_PKEY_base_id(pub_key) + if nid != nid_ec_publickey { + C.EVP_PKEY_free(pub_key) + return error('Get an nid of non ecPublicKey') + } + + eckey := C.EVP_PKEY_get1_EC_KEY(pub_key) + if eckey == 0 { + C.EC_KEY_free(eckey) + return error('Failed to get ec key') + } + // check the group for the supported curve(s) + group := C.EC_KEY_get0_group(eckey) + if group == 0 { + C.EC_GROUP_free(group) + return error('Failed to load group from key') + } + nidgroup := C.EC_GROUP_get_curve_name(group) + if nidgroup != nid_prime256v1 && nidgroup != nid_secp384r1 && nidgroup != nid_secp521r1 + && nidgroup != nid_secp256k1 { + return error('Unsupported group') + } + // Its OK to return + return PublicKey{ + key: eckey + } +} + +// bytes gets the bytes of public key parts of this keypair. +pub fn (pbk PublicKey) bytes() ![]u8 { + point := C.EC_KEY_get0_public_key(pbk.key) + if point == 0 { + C.EC_POINT_free(point) + return error('Failed to get public key BIGNUM') + } + + group := C.EC_KEY_get0_group(pbk.key) + num_bits := C.EC_GROUP_get_degree(group) + // 1 byte of conversion format || x || y of EC_POINT + num_bytes := 1 + 2 * ((num_bits + 7) / 8) + + ctx := C.BN_CTX_new() + defer { + C.BN_CTX_free(ctx) + } + + if ctx == 0 { + C.EC_POINT_free(point) + C.BN_CTX_free(ctx) + return error('Failed to create BN_CTX') + } + 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. + conv_form := C.EC_KEY_get_conv_form(pbk.key) + if conv_form !in [2, 3, 4, 6] { + return error('bad conversion format') + } + n := C.EC_POINT_point2oct(group, point, conv_form, buf.data, buf.len, ctx) + + // returns the clone of the buffer[..n] + return buf[..n].clone() +} diff --git a/vlib/crypto/ecdsa/util_test.v b/vlib/crypto/ecdsa/util_test.v new file mode 100644 index 00000000000000..97f16f8858cf01 --- /dev/null +++ b/vlib/crypto/ecdsa/util_test.v @@ -0,0 +1,37 @@ +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 +const privatekey_sample = '-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDAwzj2iiJZaxgk/C6mp +oVskdr6j7akl4bPB8JRnT1J5XNbLPK/iNd/BW+xUJEj/pxWhZANiAAT4/euEWRPV +9cdhtjcKlwF2HrFMLvgxAXFx+01UPfMQ9XOj/85qUhVq1jXraSyDy5FYF28UW4dn +04xVeRuPBbCFxc/uqYj2s5ItHcAZSV3L5sGlXadPfTqoIjCBQAx44k8= +-----END PRIVATE KEY-----' + +const public_key_sample = '-----BEGIN PUBLIC KEY----- +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE+P3rhFkT1fXHYbY3CpcBdh6xTC74MQFx +cftNVD3zEPVzo//OalIVatY162ksg8uRWBdvFFuHZ9OMVXkbjwWwhcXP7qmI9rOS +LR3AGUldy+bBpV2nT306qCIwgUAMeOJP +-----END PUBLIC KEY-----' + +// Message tobe signed and verified +const message_tobe_signed = 'Example of ECDSA with P-384' +// Message signature created with SHA384 digest with associated above key +const expected_signature = hex.decode('3066023100b08f6ec77bb319fdb7bce55a2714d7e79cc645d834ee539d8903cfcc88c6fa90df1558856cb840b2dd82e82cd89d7046023100d9d482ca8a6545a3b081fbdd4bb9643a2b4eda4e21fd624833216596032471faae646891f8d2f0bbb86b796c36d3c390')! + +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)! + assert status_without_hashed == false + + hashed_msg := sha512.sum384(message_tobe_signed.bytes()) + status_with_hashed := pbkey.verify(hashed_msg, expected_signature)! + assert status_with_hashed == true +}