Skip to content

Commit 59f24a7

Browse files
committed
kdf: add OpenSSL::KDF.derive
1 parent b8fcf0e commit 59f24a7

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.4.0
160161
have_func("TS_VERIFY_CTX_set0_certs(NULL, NULL)", ts_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)