Skip to content

Commit dce970b

Browse files
committed
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.
1 parent c9d0322 commit dce970b

File tree

3 files changed

+108
-0
lines changed

3 files changed

+108
-0
lines changed

ext/openssl/extconf.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ def find_openssl_library
155155
have_func("EVP_MD_CTX_get_pkey_ctx(NULL)", evp_h)
156156
have_func("EVP_PKEY_eq(NULL, NULL)", evp_h)
157157
have_func("EVP_PKEY_dup(NULL)", evp_h)
158+
have_type("EVP_KDF *", "openssl/types.h")
158159

159160
# added in 3.2.0
160161
have_func("SSL_get0_group_name(NULL)", ssl_h)

ext/openssl/ossl_kdf.c

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,79 @@ kdf_hkdf(int argc, VALUE *argv, VALUE self)
236236
return str;
237237
}
238238

239+
#ifdef HAVE_TYPE_EVP_KDF_P
240+
/*
241+
* call-seq:
242+
* KDF.derive(algo, length, params) -> String
243+
*
244+
* Derives _length_ bytes of key material from _params_ using the \KDF algorithm
245+
* specified by the String _algo_.
246+
*
247+
* _params_ is an Enumerable that lists the parameters and their values to be
248+
* passed to the \KDF algorithm. Consult the respective EVP_KDF-* documentation
249+
* for the available parameters.
250+
*
251+
* See the man page EVP_KDF_derive(3) for more information. Available when
252+
* compiled with \OpenSSL 3.0 or later.
253+
*
254+
* === Example
255+
* # See the man page EVP_KDF-PBKDF2(7).
256+
* # RFC 6070 PBKDF2 HMAC-SHA1 Test Vectors, 3rd example
257+
* # https://www.rfc-editor.org/rfc/rfc6070
258+
* ret = OpenSSL::KDF.derive("PBKDF2", 20, {
259+
* "pass" => "password",
260+
* "salt" => "salt",
261+
* "iter" => 4096,
262+
* "digest" => "SHA1",
263+
* })
264+
* p ret.unpack1("H*")
265+
* #=> "4b007901b765489abead49d926f721d065a429c1"
266+
*/
267+
static VALUE
268+
kdf_derive(int argc, VALUE *argv, VALUE self)
269+
{
270+
VALUE algo, keylen, hash, out;
271+
int state;
272+
273+
rb_scan_args(argc, argv, "21", &algo, &keylen, &hash);
274+
out = rb_str_new(NULL, NUM2LONG(keylen));
275+
276+
EVP_KDF *kdf = EVP_KDF_fetch(NULL, StringValueCStr(algo), NULL);
277+
if (!kdf)
278+
ossl_raise(eKDF, "EVP_KDF_fetch");
279+
280+
EVP_KDF_CTX *ctx = EVP_KDF_CTX_new(kdf);
281+
if (!ctx) {
282+
EVP_KDF_free(kdf);
283+
ossl_raise(eKDF, "EVP_KDF_CTX_new");
284+
}
285+
286+
const OSSL_PARAM *settable = EVP_KDF_CTX_settable_params(ctx);
287+
if (!settable) {
288+
EVP_KDF_CTX_free(ctx);
289+
EVP_KDF_free(kdf);
290+
ossl_raise(eKDF, "EVP_KDF_CTX_settable_params");
291+
}
292+
293+
OSSL_PARAM *params = ossl_build_params(settable, hash, &state);
294+
if (state) {
295+
EVP_KDF_CTX_free(ctx);
296+
EVP_KDF_free(kdf);
297+
rb_jump_tag(state);
298+
}
299+
300+
int ret = EVP_KDF_derive(ctx, (unsigned char *)RSTRING_PTR(out),
301+
RSTRING_LEN(out), params);
302+
OSSL_PARAM_free(params);
303+
EVP_KDF_CTX_free(ctx);
304+
EVP_KDF_free(kdf);
305+
if (ret != 1)
306+
ossl_raise(eKDF, "EVP_KDF_derive");
307+
308+
return out;
309+
}
310+
#endif
311+
239312
void
240313
Init_ossl_kdf(void)
241314
{
@@ -302,4 +375,7 @@ Init_ossl_kdf(void)
302375
rb_define_module_function(mKDF, "scrypt", kdf_scrypt, -1);
303376
#endif
304377
rb_define_module_function(mKDF, "hkdf", kdf_hkdf, -1);
378+
#ifdef HAVE_TYPE_EVP_KDF_P
379+
rb_define_module_function(mKDF, "derive", kdf_derive, -1);
380+
#endif
305381
}

test/openssl/test_kdf.rb

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,37 @@ def test_hkdf_rfc5869_test_case_4
170170
assert_equal(okm, OpenSSL::KDF.hkdf(ikm, salt: salt, info: info, length: l, hash: hash))
171171
end
172172

173+
def test_derive
174+
ret = OpenSSL::KDF.derive("PBKDF2", 20, {
175+
"pass" => "password",
176+
"salt" => "salt",
177+
"iter" => 4096,
178+
"digest" => "SHA1",
179+
})
180+
assert_equal(B("4b007901b765489abead49d926f721d065a429c1"), ret)
181+
182+
# param name not in settable_params
183+
assert_raise_with_message(OpenSSL::OpenSSLError, /unknown.*'nosucha'/) {
184+
OpenSSL::KDF.derive("PBKDF2", 20, [["nosucha", "param"]])
185+
}
186+
187+
# "pass" for PBKDF2 is an OSSL_PARAM_OCTET_STRING
188+
assert_raise_with_message(OpenSSL::OpenSSLError, /'pass'.*String value/) {
189+
OpenSSL::KDF.derive("PBKDF2", 20, [["pass", 123]])
190+
}
191+
192+
# "iter" for PBKDF2 is an OSSL_PARAM_UNSIGNED_INTEGER
193+
assert_raise_with_message(OpenSSL::OpenSSLError, /'iter'.*non-negative/) {
194+
OpenSSL::KDF.derive("PBKDF2", 20, [["iter", -1]])
195+
}
196+
197+
# "digest" for PBKDF2 is an OSSL_PARAM_UTF8_STRING, which requires a
198+
# NUL-terminated string
199+
assert_raise_with_message(ArgumentError, /string contains null byte/) {
200+
OpenSSL::KDF.derive("PBKDF2", 20, [["digest", "SHA1\0"]])
201+
}
202+
end if openssl?(3, 0, 0) || OpenSSL::KDF.respond_to?(:derive)
203+
173204
private
174205

175206
def B(ary)

0 commit comments

Comments
 (0)