Skip to content
Open
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 @@ -172,6 +172,7 @@ def find_openssl_library
have_func("EVP_PKEY_eq(NULL, NULL)", evp_h)
have_func("EVP_PKEY_dup(NULL)", evp_h)
have_func("EVP_PKEY_encapsulate_init(NULL, NULL)", evp_h)
have_func("EVP_KDF_derive(NULL, NULL, 0, NULL)", "openssl/kdf.h")

# added in 3.2.0
have_func("SSL_get0_group_name(NULL)", ssl_h)
Expand Down
106 changes: 106 additions & 0 deletions ext/openssl/ossl.c
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,112 @@ 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 make_params_args {
const OSSL_PARAM *settable;
VALUE ary;
OSSL_PARAM_BLD *bld;
};

static VALUE
make_params_push_i(RB_BLOCK_CALL_FUNC_ARGLIST(i, args_))
{
struct make_params_args *args = (struct make_params_args *)args_;

Check_Type(i, T_ARRAY);
VALUE keyv = rb_ary_entry(i, 0), obj = rb_ary_entry(i, 1);
if (SYMBOL_P(keyv))
keyv = rb_sym2str(keyv);
StringValueCStr(keyv);

const OSSL_PARAM *p =
OSSL_PARAM_locate_const(args->settable, RSTRING_PTR(keyv));
if (!p) {
VALUE keys = rb_ary_new();
for (p = args->settable; p->key; p++)
rb_ary_push(keys, rb_str_new_cstr(p->key));
rb_raise(rb_eArgError, "unrecognized OSSL_PARAM key %"PRIsVALUE" " \
"(supported keys: %"PRIsVALUE")",
keyv, rb_ary_join(keys, rb_str_new_cstr(", ")));
}

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(rb_eArgError, "OSSL_PARAM key %s expects " \
"integer value", p->key);
const BIGNUM *bn = GetBNPtr(obj);
if (p->data_type == OSSL_PARAM_UNSIGNED_INTEGER && BN_is_negative(bn))
rb_raise(rb_eArgError, "OSSL_PARAM key %s expects " \
"non-negative integer value", p->key);
if (!OSSL_PARAM_BLD_push_BN(args->bld, p->key, GetBNPtr(obj)))
ossl_raise(eOSSLError, "OSSL_PARAM_BLD_push_BN");
break;
case OSSL_PARAM_UTF8_STRING:
obj = rb_check_string_type(obj);
if (NIL_P(obj) || memchr(RSTRING_PTR(obj), 0, RSTRING_LEN(obj)))
rb_raise(rb_eArgError, "OSSL_PARAM key %s expects " \
"NUL-terminated string value", p->key);
if (!OSSL_PARAM_BLD_push_utf8_string(args->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(rb_eArgError, "OSSL_PARAM key %s expects string value",
p->key);
if (!OSSL_PARAM_BLD_push_octet_string(args->bld, p->key, RSTRING_PTR(obj),
RSTRING_LEN(obj)))
ossl_raise(eOSSLError, "OSSL_PARAM_BLD_push_octet_string");
break;
default:
/*
* As of OpenSSL 4.0, the following data types are defined, but are
* not actually used by the builtin providers. So leave them
* unimplemented for now:
* - OSSL_PARAM_REAL (double)
* - OSSL_PARAM_UTF8_PTR (C string)
* - OSSL_PARAM_OCTET_PTR (unknown)
*/
rb_raise(eOSSLError, "unsupported OSSL_PARAM data type %d for key %s",
p->data_type, p->key);
}
return Qnil;
}

static VALUE
make_params_i(VALUE args_)
{
struct make_params_args *args = (struct make_params_args *)args_;

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

rb_block_call(args->ary, rb_intern("each"), 0, NULL, make_params_push_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_make_params(const OSSL_PARAM *settable, VALUE ary, int *state)
{
struct make_params_args args = { settable, ary, NULL };
VALUE params = rb_protect(make_params_i, (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 @@ -166,6 +166,14 @@ void ossl_clear_error(void);
VALUE ossl_to_der(VALUE);
VALUE ossl_to_der_if_possible(VALUE);

#ifdef OSSL_PARAM_INTEGER
/*
* Make an OSSL_PARAM array from Hash/Enumerable. The OSSL_PARAM array is
* allocated by OpenSSL's malloc and must be freed by OSSL_PARAM_free().
*/
OSSL_PARAM *ossl_make_params(const OSSL_PARAM *settable, VALUE ary, 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 @@ -114,8 +114,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 @@ -137,7 +137,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 @@ -1047,7 +1047,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
99 changes: 99 additions & 0 deletions ext/openssl/ossl_kdf.c
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,104 @@ kdf_hkdf(int argc, VALUE *argv, VALUE self)
return str;
}

#ifdef HAVE_EVP_KDF_DERIVE
struct kdf_derive_args {
EVP_KDF_CTX *ctx;
unsigned char *out;
size_t outlen;
OSSL_PARAM *params;
};

static void *
kdf_derive_nogvl(void *args_)
{
struct kdf_derive_args *args = args_;
int ret = EVP_KDF_derive(args->ctx, args->out, args->outlen, args->params);
return (void *)(uintptr_t)ret;
}

/*
* 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_. This is a low-level interface to provide
* access to the +EVP_KDF+ API available with \OpenSSL 3.0 or later.
*
* _params_ specifies the set of +OSSL_PARAM+ to be passed to EVP_KDF_derive(3).
* Check the relevant EVP_KDF-* man page, or the documentation for the OpenSSL
* provider in use, for the supported parameters.
*
* See the man page EVP_KDF_derive(3) for details.
*
* === 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, ary, out;
EVP_KDF *kdf;
EVP_KDF_CTX *ctx;
OSSL_PARAM *params = NULL;

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

kdf = EVP_KDF_fetch(NULL, StringValueCStr(algo), NULL);
if (!kdf)
ossl_raise(eKDF, "EVP_KDF_fetch");
ctx = EVP_KDF_CTX_new(kdf);
if (!ctx) {
EVP_KDF_free(kdf);
ossl_raise(eKDF, "EVP_KDF_CTX_new");
}
if (!NIL_P(ary)) {
const OSSL_PARAM *settable = EVP_KDF_CTX_settable_params(ctx);
int state;
if (!settable) {
EVP_KDF_CTX_free(ctx);
EVP_KDF_free(kdf);
ossl_raise(eKDF, "EVP_KDF_CTX_settable_params");
}
params = ossl_make_params(settable, ary, &state);
if (state) {
EVP_KDF_CTX_free(ctx);
EVP_KDF_free(kdf);
rb_jump_tag(state);
}
}

struct kdf_derive_args args = {
.ctx = ctx,
.out = (unsigned char *)RSTRING_PTR(out),
.outlen = RSTRING_LEN(out),
.params = params,
};
int ret = (int)(uintptr_t)rb_thread_call_without_gvl(kdf_derive_nogvl,
&args, NULL, NULL);
OSSL_PARAM_free(params);
EVP_KDF_CTX_free(ctx);
EVP_KDF_free(kdf);
if (ret != 1)
ossl_raise(eKDF, "EVP_KDF_derive");

return out;
}
#else
#define kdf_derive rb_f_notimplement
#endif

void
Init_ossl_kdf(void)
{
Expand Down Expand Up @@ -373,4 +471,5 @@ Init_ossl_kdf(void)
rb_define_module_function(mKDF, "scrypt", kdf_scrypt, -1);
#endif
rb_define_module_function(mKDF, "hkdf", kdf_hkdf, -1);
rb_define_module_function(mKDF, "derive", kdf_derive, -1);
}
1 change: 1 addition & 0 deletions lib/openssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
require_relative 'openssl/cipher'
require_relative 'openssl/digest'
require_relative 'openssl/hmac'
require_relative 'openssl/kdf'
require_relative 'openssl/pkcs5'
require_relative 'openssl/pkey'
require_relative 'openssl/ssl'
Expand Down
40 changes: 40 additions & 0 deletions lib/openssl/kdf.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module OpenSSL
module KDF
# Argon2id, a variant of Argon2, is a password hashing function
# described in {RFC 9106}[https://www.rfc-editor.org/rfc/rfc9106].
#
# This methods requires \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. Optional. +K+ in RFC 9106.
# ad:: Associated data. Optional. +X+ in RFC 9106.
#
# === Example
# password = "\x01" * 32
# salt = "\x02" * 16
# secret = "\x03" * 8
# ad = "\x04" * 12
# ret = OpenSSL::KDF.argon2id(
# password, salt: salt, lanes: 4, length: 32,
# memcost: 32, iter: 3, secret: secret, ad: ad,
# )
# p ret.unpack1("H*")
# #=> "0d640df58d78766c08c037a34a8b53c9d01ef0452d75b65eb52520e96b01e659"
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
58 changes: 58 additions & 0 deletions test/openssl/test_kdf.rb
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,64 @@ def test_hkdf_rfc5869_test_case_5
assert_equal(okm, OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: l, hash: hash))
end

def test_derive
unless openssl?(3, 0, 0) || OpenSSL::KDF.respond_to?(:derive)
omit "EVP_KDF_derive() is not supported"
end

# https://www.rfc-editor.org/rfc/rfc6070.html#section-2
# PBKDF2 HMAC-SHA1 Test Vectors, 5th example
params = [
["pass", "passwordPASSWORDpassword"],
["salt", "saltSALTsaltSALTsaltSALTsaltSALTsalt"],
["iter", 4096],
["digest", "SHA1"],
]
dk = B("3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038")
assert_equal(dk, OpenSSL::KDF.derive("PBKDF2", 25, params))

params_hash = params.map { |k, v| [k.to_sym, v] }.to_h
assert_equal(dk, OpenSSL::KDF.derive("PBKDF2", 25, params_hash))

# param key not in settable_params
assert_raise_with_message(ArgumentError, /nosucha.*iter/) {
OpenSSL::KDF.derive("PBKDF2", 20, [["nosucha", "param"]])
}

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

# "iter" for PBKDF2 is an OSSL_PARAM_UNSIGNED_INTEGER
assert_raise_with_message(ArgumentError, /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, /digest.*NUL-terminated/) {
OpenSSL::KDF.derive("PBKDF2", 20, [["digest", "SHA1\0"]])
}
end

def test_argon2id_rfc9106
omit_on_fips
omit "Argon2id is not supported" unless openssl?(3, 2, 0)

# https://www.rfc-editor.org/rfc/rfc9106.html#section-5.3
# 5.3. Argon2id Test Vectors
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

private

def B(ary)
Expand Down
Loading