Skip to content

Add OpenSSL::KDF.derive to expose EVP_KDF in OpenSSL 3.0 #906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ext/openssl/extconf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def find_openssl_library
have_func("EVP_MD_CTX_get_pkey_ctx(NULL)", evp_h)
have_func("EVP_PKEY_eq(NULL, NULL)", evp_h)
have_func("EVP_PKEY_dup(NULL)", evp_h)
have_type("EVP_KDF *", "openssl/types.h")

# added in 3.4.0
have_func("TS_VERIFY_CTX_set0_certs(NULL, NULL)", ts_h)
Expand Down
108 changes: 108 additions & 0 deletions ext/openssl/ossl.c
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,114 @@ ossl_pem_passwd_cb(char *buf, int max_len, int flag, void *pwd_)
return (int)len;
}

#ifdef OSSL_PARAM_INTEGER
#include <openssl/param_build.h>

struct build_params_args {
const OSSL_PARAM *settable;
VALUE hash;
OSSL_PARAM_BLD *bld;
};

static VALUE
build_params_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, memo))
{
struct build_params_args *args = (struct build_params_args *)memo;
VALUE keyv = rb_ary_entry(i, 0), obj = rb_ary_entry(i, 1);
OSSL_PARAM_BLD *bld = args->bld;

if (SYMBOL_P(keyv))
keyv = rb_sym2str(keyv);

const OSSL_PARAM *p = args->settable;
const char *key = StringValueCStr(keyv);
while (p->key && strcmp(p->key, key))
p++;
if (!p->key)
rb_raise(eOSSLError, "unknown OSSL_PARAM key '%"PRIsVALUE"'", keyv);

switch (p->data_type) {
case OSSL_PARAM_INTEGER:
case OSSL_PARAM_UNSIGNED_INTEGER:
obj = ossl_try_convert_to_bn(obj);
if (NIL_P(obj))
rb_raise(eOSSLError, "OSSL_PARAM key '%s' requires " \
"an integer value", p->key);
const BIGNUM *bn = GetBNPtr(obj);
if (p->data_type == OSSL_PARAM_UNSIGNED_INTEGER && BN_is_negative(bn))
rb_raise(eOSSLError, "OSSL_PARAM key '%s' requires " \
"a non-negative integer value", p->key);
if (!OSSL_PARAM_BLD_push_BN(bld, p->key, GetBNPtr(obj)))
ossl_raise(eOSSLError, "OSSL_PARAM_BLD_push_BN");
break;
case OSSL_PARAM_REAL:
obj = rb_check_to_float(obj);
if (NIL_P(obj))
rb_raise(eOSSLError, "OSSL_PARAM key '%s' requires a Float value",
p->key);
if (!OSSL_PARAM_BLD_push_double(bld, p->key, NUM2DBL(obj)))
ossl_raise(eOSSLError, "OSSL_PARAM_BLD_push_double");
break;
case OSSL_PARAM_UTF8_STRING:
obj = rb_check_string_type(obj);
if (NIL_P(obj))
rb_raise(eOSSLError, "OSSL_PARAM key '%s' requires a " \
"NUL-terminated String value", p->key);
StringValueCStr(obj);
if (!OSSL_PARAM_BLD_push_utf8_string(bld, p->key, RSTRING_PTR(obj),
RSTRING_LEN(obj)))
ossl_raise(eOSSLError, "OSSL_PARAM_BLD_push_utf8_string");
break;
case OSSL_PARAM_OCTET_STRING:
obj = rb_check_string_type(obj);
if (NIL_P(obj))
rb_raise(eOSSLError, "OSSL_PARAM key '%s' requires a String value",
p->key);
if (!OSSL_PARAM_BLD_push_octet_string(bld, p->key, RSTRING_PTR(obj),
RSTRING_LEN(obj)))
ossl_raise(eOSSLError, "OSSL_PARAM_BLD_push_octet_string");
break;
default:
/*
* Types not used in settable OSSL_PARAMs as of OpenSSL 3.5:
* - OSSL_PARAM_UTF8_PTR
* - OSSL_PARAM_OCTET_PTR
*/
rb_raise(eOSSLError, "unsupported type %d for OSSL_PARAM key '%s'",
p->data_type, p->key);
}
return Qnil;
}

static VALUE
build_params_internal(VALUE memo)
{
struct build_params_args *args = (struct build_params_args *)memo;

args->bld = OSSL_PARAM_BLD_new();
if (!args->bld)
ossl_raise(eOSSLError, "OSSL_PARAM_BLD_new");

if (!NIL_P(args->hash))
rb_block_call(args->hash, rb_intern("each"), 0, NULL, build_params_i,
(VALUE)args);

OSSL_PARAM *ret = OSSL_PARAM_BLD_to_param(args->bld);
if (!ret)
ossl_raise(eOSSLError, "OSSL_PARAM_BLD_to_param");
return (VALUE)ret;
}

OSSL_PARAM *
ossl_build_params(const OSSL_PARAM *settable, VALUE hash, int *state)
{
struct build_params_args args = { settable, hash, NULL };
VALUE params = rb_protect(build_params_internal, (VALUE)&args, state);
OSSL_PARAM_BLD_free(args.bld);
return (OSSL_PARAM *)params;
}
#endif

/*
* main module
*/
Expand Down
8 changes: 8 additions & 0 deletions ext/openssl/ossl.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@ void ossl_clear_error(void);
VALUE ossl_to_der(VALUE);
VALUE ossl_to_der_if_possible(VALUE);

#ifdef OSSL_PARAM_INTEGER
/*
* Build OSSL_PARAM array from Hash/Enumerable. The OSSL_PARAM array is
* allocated by OpenSSL, so it must be freed by OSSL_PARAM_free() after use.
*/
OSSL_PARAM *ossl_build_params(const OSSL_PARAM *settable, VALUE hash, int *state);
#endif

/*
* Debug
*/
Expand Down
8 changes: 4 additions & 4 deletions ext/openssl/ossl_bn.c
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ integer_to_bnptr(VALUE obj, BIGNUM *orig)
return bn;
}

static VALUE
try_convert_to_bn(VALUE obj)
VALUE
ossl_try_convert_to_bn(VALUE obj)
{
BIGNUM *bn;
VALUE newobj = Qnil;
Expand All @@ -138,7 +138,7 @@ ossl_bn_value_ptr(volatile VALUE *ptr)
VALUE tmp;
BIGNUM *bn;

tmp = try_convert_to_bn(*ptr);
tmp = ossl_try_convert_to_bn(*ptr);
if (NIL_P(tmp))
ossl_raise(rb_eTypeError, "Cannot convert into OpenSSL::BN");
GetBN(tmp, bn);
Expand Down Expand Up @@ -1048,7 +1048,7 @@ ossl_bn_eq(VALUE self, VALUE other)
BIGNUM *bn1, *bn2;

GetBN(self, bn1);
other = try_convert_to_bn(other);
other = ossl_try_convert_to_bn(other);
if (NIL_P(other))
return Qfalse;
GetBN(other, bn2);
Expand Down
1 change: 1 addition & 0 deletions ext/openssl/ossl_bn.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ BN_CTX *ossl_bn_ctx_get(void);
#define GetBNPtr(obj) ossl_bn_value_ptr(&(obj))

VALUE ossl_bn_new(const BIGNUM *);
VALUE ossl_try_convert_to_bn(VALUE obj);
BIGNUM *ossl_bn_value_ptr(volatile VALUE *);
void Init_ossl_bn(void);

Expand Down
76 changes: 76 additions & 0 deletions ext/openssl/ossl_kdf.c
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,79 @@ kdf_hkdf(int argc, VALUE *argv, VALUE self)
return str;
}

#ifdef HAVE_TYPE_EVP_KDF_P
/*
* call-seq:
* KDF.derive(algo, length, params) -> String
*
* Derives _length_ bytes of key material from _params_ using the \KDF algorithm
* specified by the String _algo_.
*
* _params_ is an Enumerable that lists the parameters and their values to be
* passed to the \KDF algorithm. Consult the respective EVP_KDF-* documentation
* for the available parameters.
*
* See the man page EVP_KDF_derive(3) for more information. Available when
* compiled with \OpenSSL 3.0 or later.
*
* === Example
* # See the man page EVP_KDF-PBKDF2(7).
* # RFC 6070 PBKDF2 HMAC-SHA1 Test Vectors, 3rd example
* # https://www.rfc-editor.org/rfc/rfc6070
* ret = OpenSSL::KDF.derive("PBKDF2", 20, {
* "pass" => "password",
* "salt" => "salt",
* "iter" => 4096,
* "digest" => "SHA1",
* })
* p ret.unpack1("H*")
* #=> "4b007901b765489abead49d926f721d065a429c1"
*/
static VALUE
kdf_derive(int argc, VALUE *argv, VALUE self)
{
VALUE algo, keylen, hash, out;
int state;

rb_scan_args(argc, argv, "21", &algo, &keylen, &hash);
out = rb_str_new(NULL, NUM2LONG(keylen));

EVP_KDF *kdf = EVP_KDF_fetch(NULL, StringValueCStr(algo), NULL);
if (!kdf)
ossl_raise(eKDF, "EVP_KDF_fetch");

EVP_KDF_CTX *ctx = EVP_KDF_CTX_new(kdf);
if (!ctx) {
EVP_KDF_free(kdf);
ossl_raise(eKDF, "EVP_KDF_CTX_new");
}

const OSSL_PARAM *settable = EVP_KDF_CTX_settable_params(ctx);
if (!settable) {
EVP_KDF_CTX_free(ctx);
EVP_KDF_free(kdf);
ossl_raise(eKDF, "EVP_KDF_CTX_settable_params");
}

OSSL_PARAM *params = ossl_build_params(settable, hash, &state);
if (state) {
EVP_KDF_CTX_free(ctx);
EVP_KDF_free(kdf);
rb_jump_tag(state);
}

int ret = EVP_KDF_derive(ctx, (unsigned char *)RSTRING_PTR(out),
RSTRING_LEN(out), params);
OSSL_PARAM_free(params);
EVP_KDF_CTX_free(ctx);
EVP_KDF_free(kdf);
if (ret != 1)
ossl_raise(eKDF, "EVP_KDF_derive");

return out;
}
#endif

void
Init_ossl_kdf(void)
{
Expand Down Expand Up @@ -302,4 +375,7 @@ Init_ossl_kdf(void)
rb_define_module_function(mKDF, "scrypt", kdf_scrypt, -1);
#endif
rb_define_module_function(mKDF, "hkdf", kdf_hkdf, -1);
#ifdef HAVE_TYPE_EVP_KDF_P
rb_define_module_function(mKDF, "derive", kdf_derive, -1);
#endif
}
1 change: 1 addition & 0 deletions lib/openssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
require_relative 'openssl/ssl'
require_relative 'openssl/pkcs5'
require_relative 'openssl/version'
require_relative 'openssl/kdf'

module OpenSSL
# call-seq:
Expand Down
30 changes: 30 additions & 0 deletions lib/openssl/kdf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# frozen_string_literal: true

module OpenSSL
module KDF
if respond_to?(:derive)
# Argon2id, a variant of Argon2, is a password hashing function
# described in {RFC 9106}[https://www.rfc-editor.org/rfc/rfc9106].
#
# Available when compiled with \OpenSSL 3.2 or later.
#
# === Parameters
# pass:: Passowrd to be hashed. Message string +P+ in RFC 9106.
# salt:: Salt. Nonce +S+ in RFC 9106.
# lanes:: Degree of parallelism. +p+ in RFC 9106.
# length:: Desired output length in bytes. Tag length +T+ in RFC 9106.
# memcost:: Memory size in the number of kibibytes. +m+ in RFC 9106.
# iter:: Number of passes. +t+ in RFC 9106.
# secret:: Secret value. +K+ in RFC 9106.
# ad:: Associated data. +X+ in RFC 9106.
def self.argon2id(pass, salt:, lanes:, length:, memcost:, iter:,
secret: "", ad: "")
params = {
pass: pass, salt: salt, lanes: lanes, memcost: memcost, iter: iter,
secret: secret, ad: ad,
}
derive("ARGON2ID", length, params)
end
end
end
end
43 changes: 43 additions & 0 deletions test/openssl/test_kdf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,49 @@ def test_hkdf_rfc5869_test_case_4
assert_equal(okm, OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: l, hash: hash))
end

def test_derive
ret = OpenSSL::KDF.derive("PBKDF2", 20, {
"pass" => "password",
"salt" => "salt",
"iter" => 4096,
"digest" => "SHA1",
})
assert_equal(B("4b007901b765489abead49d926f721d065a429c1"), ret)

# param name not in settable_params
assert_raise_with_message(OpenSSL::OpenSSLError, /unknown.*'nosucha'/) {
OpenSSL::KDF.derive("PBKDF2", 20, [["nosucha", "param"]])
}

# "pass" for PBKDF2 is an OSSL_PARAM_OCTET_STRING
assert_raise_with_message(OpenSSL::OpenSSLError, /'pass'.*String value/) {
OpenSSL::KDF.derive("PBKDF2", 20, [["pass", 123]])
}

# "iter" for PBKDF2 is an OSSL_PARAM_UNSIGNED_INTEGER
assert_raise_with_message(OpenSSL::OpenSSLError, /'iter'.*non-negative/) {
OpenSSL::KDF.derive("PBKDF2", 20, [["iter", -1]])
}

# "digest" for PBKDF2 is an OSSL_PARAM_UTF8_STRING, which requires a
# NUL-terminated string
assert_raise_with_message(ArgumentError, /string contains null byte/) {
OpenSSL::KDF.derive("PBKDF2", 20, [["digest", "SHA1\0"]])
}
end if openssl?(3, 0, 0) || OpenSSL::KDF.respond_to?(:derive)

def test_argon2id_rfc9106
password = B("01" * 32)
salt = B("02" * 16)
secret = B("03" * 8)
ad = B("04" * 12)
tag = B("0d640df58d78766c08c037a34a8b53c9d0" \
"1ef0452d75b65eb52520e96b01e659")
ret = OpenSSL::KDF.argon2id(password, salt: salt, lanes: 4, length: 32,
memcost: 32, iter: 3, secret: secret, ad: ad)
assert_equal(tag, ret)
end if openssl?(3, 2, 0)

private

def B(ary)
Expand Down