From eb3998728a060b51625d25c52de0b7cd667a8e5a Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sun, 6 Jul 2025 02:25:26 +0900 Subject: [PATCH 1/4] lib/openssl.rb: require files in alphabetical order This list was originally in alphabetical order. Sort it again. This change should be safe since the .rb sources should only depend on the extension and not each other. --- lib/openssl.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/openssl.rb b/lib/openssl.rb index 088992389..48f88dcbb 100644 --- a/lib/openssl.rb +++ b/lib/openssl.rb @@ -12,16 +12,16 @@ require 'openssl.so' -require_relative 'openssl/bn' require_relative 'openssl/asn1' -require_relative 'openssl/pkey' +require_relative 'openssl/bn' require_relative 'openssl/cipher' require_relative 'openssl/digest' require_relative 'openssl/hmac' -require_relative 'openssl/x509' -require_relative 'openssl/ssl' require_relative 'openssl/pkcs5' +require_relative 'openssl/pkey' +require_relative 'openssl/ssl' require_relative 'openssl/version' +require_relative 'openssl/x509' module OpenSSL # call-seq: From c9d0322a543555e3d3b1fb2a9de2c2b2158a667d Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sat, 24 May 2025 05:31:36 +0900 Subject: [PATCH 2/4] ossl.c: add ossl_build_params() Provide a useful function to make a heap-allocated array of OSSL_PARAM from Enumerable. OSSL_PARAM is a new type added in OpenSSL 3.0 and used by various new APIs for taking arbitrary parameters. The next patch will use this with EVP_KDF to implement OpenSSL::KDF.derive. --- ext/openssl/ossl.c | 108 ++++++++++++++++++++++++++++++++++++++++++ ext/openssl/ossl.h | 8 ++++ ext/openssl/ossl_bn.c | 8 ++-- ext/openssl/ossl_bn.h | 1 + 4 files changed, 121 insertions(+), 4 deletions(-) diff --git a/ext/openssl/ossl.c b/ext/openssl/ossl.c index 60780790b..bfc0820e5 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(rb_eArgError, "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(rb_eArgError, "OSSL_PARAM key '%s' requires " \ + "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' requires " \ + "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(rb_eArgError, "OSSL_PARAM key '%s' requires 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(rb_eArgError, "OSSL_PARAM key '%s' requires " \ + "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(rb_eArgError, "OSSL_PARAM key '%s' requires 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); From dce970b3309f926723509e56d482a89f372d0d71 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sat, 24 May 2025 05:32:25 +0900 Subject: [PATCH 3/4] kdf: add OpenSSL::KDF.derive Expose EVP_KDF_derive() added in OpenSSL 3.0. OpenSSL apparently plans to implement new algorithms through this interface only from now on. For example, the Argon2 password hashing algorithm added in OpenSSL 3.2 is available exclusively through this API. This is a low-level and minimum method to interact with the API. You will have to carefully read the relevant man pages to use this correctly. --- ext/openssl/extconf.rb | 1 + ext/openssl/ossl_kdf.c | 76 ++++++++++++++++++++++++++++++++++++++++ test/openssl/test_kdf.rb | 31 ++++++++++++++++ 3 files changed, 108 insertions(+) diff --git a/ext/openssl/extconf.rb b/ext/openssl/extconf.rb index 8aac52ef4..8ed22b6d1 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.2.0 have_func("SSL_get0_group_name(NULL)", ssl_h) 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/test/openssl/test_kdf.rb b/test/openssl/test_kdf.rb index 6a12a25aa..bc6501bf8 100644 --- a/test/openssl/test_kdf.rb +++ b/test/openssl/test_kdf.rb @@ -170,6 +170,37 @@ 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) + private def B(ary) From cac4b645d4309b1a6a7d305cd53a74ba245897f3 Mon Sep 17 00:00:00 2001 From: Kazuki Yamaguchi Date: Sat, 24 May 2025 05:32:40 +0900 Subject: [PATCH 4/4] kdf: add a shorthand method for Argon2id --- lib/openssl.rb | 1 + lib/openssl/kdf.rb | 42 ++++++++++++++++++++++++++++++++++++++++ test/openssl/test_kdf.rb | 14 ++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 lib/openssl/kdf.rb diff --git a/lib/openssl.rb b/lib/openssl.rb index 48f88dcbb..0d1c1f1ff 100644 --- a/lib/openssl.rb +++ b/lib/openssl.rb @@ -17,6 +17,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' diff --git a/lib/openssl/kdf.rb b/lib/openssl/kdf.rb new file mode 100644 index 000000000..502c95f90 --- /dev/null +++ b/lib/openssl/kdf.rb @@ -0,0 +1,42 @@ +# 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]. + # + # 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 +end diff --git a/test/openssl/test_kdf.rb b/test/openssl/test_kdf.rb index bc6501bf8..1aa5ded90 100644 --- a/test/openssl/test_kdf.rb +++ b/test/openssl/test_kdf.rb @@ -201,6 +201,20 @@ def test_derive } end if openssl?(3, 0, 0) || OpenSSL::KDF.respond_to?(:derive) + def test_argon2id_rfc9106 + # 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 if openssl?(3, 2, 0) + private def B(ary)