diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index afbed10b5..192ef3323 100644 --- a/ext/openssl/extconf.rb +++ b/ext/openssl/extconf.rb @@ -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) diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c index 60780790b..0d994d68e 100644 --- a/ext/openssl/ossl.c +++ b/ext/openssl/ossl.c @@ -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 + +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 */ diff --git a/ext/openssl/ossl.h b/ext/openssl/ossl.h index 22471d208..cfd089b33 100644 --- a/ext/openssl/ossl.h +++ b/ext/openssl/ossl.h @@ -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 */ diff --git a/ext/openssl/ossl_bn.c b/ext/openssl/ossl_bn.c index 8699ce8ec..9e10363b3 100644 --- a/ext/openssl/ossl_bn.c +++ b/ext/openssl/ossl_bn.c @@ -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; @@ -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); @@ -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); diff --git a/ext/openssl/ossl_bn.h b/ext/openssl/ossl_bn.h index 0c186bd1c..38a7e51e7 100644 --- a/ext/openssl/ossl_bn.h +++ b/ext/openssl/ossl_bn.h @@ -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); diff --git a/ext/openssl/ossl_kdf.c b/ext/openssl/ossl_kdf.c index f349939a8..38500ad6b 100644 --- a/ext/openssl/ossl_kdf.c +++ b/ext/openssl/ossl_kdf.c @@ -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) { @@ -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 } diff --git a/lib/openssl.rb b/lib/openssl.rb index 088992389..8e3eb609a 100644 --- a/lib/openssl.rb +++ b/lib/openssl.rb @@ -22,6 +22,7 @@ require_relative 'openssl/ssl' require_relative 'openssl/pkcs5' require_relative 'openssl/version' +require_relative 'openssl/kdf' module OpenSSL # call-seq: diff --git a/lib/openssl/kdf.rb b/lib/openssl/kdf.rb new file mode 100644 index 000000000..1fb349f9a --- /dev/null +++ b/lib/openssl/kdf.rb @@ -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 diff --git a/test/openssl/test_kdf.rb b/test/openssl/test_kdf.rb index 6a12a25aa..0278c051e 100644 --- a/test/openssl/test_kdf.rb +++ b/test/openssl/test_kdf.rb @@ -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)