crypto.ecdsa: improve safety checking, unify signing (and verifying) api to accept options (#23463)

This commit is contained in:
blackshirt 2025-01-19 01:07:19 +07:00 committed by GitHub
parent 3c4878063e
commit c2b7dbf9b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 752 additions and 109 deletions

View file

@ -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.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.BN_bn2binpad(a &C.BIGNUM, to &u8, tolen int) int
fn C.BN_cmp(a &C.BIGNUM, b &C.BIGNUM) int
fn C.BN_CTX_new() &C.BN_CTX
fn C.BN_CTX_free(ctx &C.BN_CTX)
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
fn C.EC_KEY_get0_private_key(key &C.EC_KEY) &C.BIGNUM
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_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
// 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 {}
pub struct PrivateKey {
key &C.EC_KEY
// 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
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
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 a message with private key
// FIXME: should the message should be hashed?
pub fn (priv_key PrivateKey) sign(message []u8) ![]u8 {
// 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_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)
}