Skip to content

Commit e87750e

Browse files
xnoxclaude
andcommitted
Add pkcs5 argument to OpenSSL::KDF.pbkdf2_hmac
With OpenSSL 3.0 and later, derive the key with EVP_KDF_fetch() and EVP_KDF_derive(), passing all arguments as OSSL_PARAMs, including OSSL_KDF_PARAM_PKCS5. The new pkcs5: keyword argument controls the SP 800-132 lower bound checks: 0 enforces them, and 1 (the default) bypasses them. The default matches the behavior of PKCS5_PBKDF2_HMAC() prior to the OpenSSL 4.0 release, and allows existing data to be decrypted, such as Active Record encrypted attributes, which use a salt that is too short by default (rails/rails#58937). Other libraries continue to use PKCS5_PBKDF2_HMAC() and ignore the argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e3cc410 commit e87750e

2 files changed

Lines changed: 94 additions & 5 deletions

File tree

‎ext/openssl/ossl_kdf.c‎

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
*/
55
#include "ossl.h"
66
#include <openssl/kdf.h>
7+
#ifdef OSSL_USE_PROVIDER
8+
# include <openssl/core_names.h>
9+
#endif
710

811
static VALUE mKDF, eKDF;
912

@@ -16,21 +19,53 @@ struct pbkdf2_hmac_args {
1619
const EVP_MD *md;
1720
int len;
1821
unsigned char *out;
22+
int pkcs5;
1923
};
2024

2125
static void *
2226
pbkdf2_hmac_nogvl(void *args_)
2327
{
2428
struct pbkdf2_hmac_args *args = (struct pbkdf2_hmac_args *)args_;
29+
#ifdef OSSL_USE_PROVIDER
30+
static char empty[] = "";
31+
int ret = 0;
32+
EVP_KDF *kdf;
33+
EVP_KDF_CTX *kctx;
34+
OSSL_PARAM params[6], *p = params;
35+
36+
kdf = EVP_KDF_fetch(NULL, OSSL_KDF_NAME_PBKDF2, NULL);
37+
if (kdf == NULL)
38+
return (void *)(uintptr_t)0;
39+
kctx = EVP_KDF_CTX_new(kdf);
40+
EVP_KDF_free(kdf);
41+
if (kctx == NULL)
42+
return (void *)(uintptr_t)0;
43+
*p++ = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_PKCS5, &args->pkcs5);
44+
*p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_PASSWORD,
45+
args->passlen ? args->pass : empty,
46+
(size_t)args->passlen);
47+
*p++ = OSSL_PARAM_construct_octet_string(OSSL_KDF_PARAM_SALT,
48+
args->saltlen ? (char *)args->salt : empty,
49+
(size_t)args->saltlen);
50+
*p++ = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_ITER, &args->iters);
51+
*p++ = OSSL_PARAM_construct_utf8_string(OSSL_KDF_PARAM_DIGEST,
52+
(char *)EVP_MD_get0_name(args->md),
53+
0);
54+
*p = OSSL_PARAM_construct_end();
55+
if (EVP_KDF_derive(kctx, args->out, (size_t)args->len, params) == 1)
56+
ret = 1;
57+
EVP_KDF_CTX_free(kctx);
58+
#else
2559
int ret = PKCS5_PBKDF2_HMAC(args->pass, args->passlen, args->salt,
2660
args->saltlen, args->iters, args->md,
2761
args->len, args->out);
62+
#endif
2863
return (void *)(uintptr_t)ret;
2964
}
3065

3166
/*
3267
* call-seq:
33-
* KDF.pbkdf2_hmac(pass, salt:, iterations:, length:, hash:) -> aString
68+
* KDF.pbkdf2_hmac(pass, salt:, iterations:, length:, hash:, pkcs5: 1) -> aString
3469
*
3570
* PKCS #5 PBKDF2 (Password-Based Key Derivation Function 2) in combination
3671
* with HMAC. Takes _pass_, _salt_ and _iterations_, and then derives a key
@@ -56,29 +91,57 @@ pbkdf2_hmac_nogvl(void *args_)
5691
* hash :: The hash algorithm used with HMAC for the PRF. May be a String
5792
* representing the algorithm name, or an instance of
5893
* OpenSSL::Digest.
94+
* pkcs5 :: Optional. Enforces or bypasses the SP 800-132 lower bound
95+
* checks done by OpenSSL. If 1 (the default) or any other
96+
* non-zero value, the checks are bypassed, for compatibility
97+
* with plain PKCS #5. If 0, OpenSSL 3.0 and later enforce the
98+
* lower bound checks: an iteration count of at least 1000, a
99+
* salt of at least 128 bits, a derived key of at least 112
100+
* bits, and a non-empty password (at least 8 bytes with the
101+
* FIPS provider). With the FIPS provider, bypassing the checks
102+
* might still block the operation, or raise the non-approved
103+
* usage indicator, which allows it for decryption and legacy
104+
* purposes. With other libraries, such as LibreSSL, this
105+
* argument is accepted but has no effect.
106+
*
107+
* The default matches the behavior of PKCS5_PBKDF2_HMAC()
108+
* prior to the OpenSSL 4.0 release. It allows existing data
109+
* to be decrypted, such as Active Record encrypted attributes,
110+
* which use a salt that is too short by default
111+
* (https://github.com/rails/rails/pull/58937).
112+
*
113+
* *NOTE*: The lower bounds enforced by OpenSSL are minimums, not
114+
* recommendations. They are far too low to protect stored passwords
115+
* against modern brute-force attacks. For security-sensitive uses, choose
116+
* parameters as recommended by the OWASP Password Storage Cheat Sheet
117+
* (https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html),
118+
* for example 600,000 iterations with HMAC-SHA-256 or 210,000 iterations
119+
* with HMAC-SHA-512.
59120
*/
60121
static VALUE
61122
kdf_pbkdf2_hmac(int argc, VALUE *argv, VALUE self)
62123
{
63-
VALUE pass, salt, opts, kwargs[4], str, md_holder, pass_tmp, salt_tmp;
64-
static ID kwargs_ids[4];
65-
int passlen, saltlen, iters, len;
124+
VALUE pass, salt, opts, kwargs[5], str, md_holder, pass_tmp, salt_tmp;
125+
static ID kwargs_ids[5];
126+
int passlen, saltlen, iters, len, pkcs5;
66127
const EVP_MD *md;
67128

68129
if (!kwargs_ids[0]) {
69130
kwargs_ids[0] = rb_intern_const("salt");
70131
kwargs_ids[1] = rb_intern_const("iterations");
71132
kwargs_ids[2] = rb_intern_const("length");
72133
kwargs_ids[3] = rb_intern_const("hash");
134+
kwargs_ids[4] = rb_intern_const("pkcs5");
73135
}
74136
rb_scan_args(argc, argv, "1:", &pass, &opts);
75-
rb_get_kwargs(opts, kwargs_ids, 4, 0, kwargs);
137+
rb_get_kwargs(opts, kwargs_ids, 4, 1, kwargs);
76138

77139
StringValue(pass);
78140
salt = StringValue(kwargs[0]);
79141
iters = NUM2INT(kwargs[1]);
80142
len = NUM2INT(kwargs[2]);
81143
md = ossl_evp_md_fetch(kwargs[3], &md_holder);
144+
pkcs5 = kwargs[4] == Qundef ? 1 : NUM2INT(kwargs[4]);
82145
passlen = RSTRING_LENINT(pass);
83146
saltlen = RSTRING_LENINT(salt);
84147
str = rb_str_new(NULL, len);
@@ -91,11 +154,16 @@ kdf_pbkdf2_hmac(int argc, VALUE *argv, VALUE self)
91154
.md = md,
92155
.len = len,
93156
.out = (unsigned char *)RSTRING_PTR(str),
157+
.pkcs5 = pkcs5,
94158
};
95159
memcpy(args.pass, RSTRING_PTR(pass), passlen);
96160
memcpy(args.salt, RSTRING_PTR(salt), saltlen);
97161
if (!rb_thread_call_without_gvl(pbkdf2_hmac_nogvl, &args, NULL, NULL))
162+
#ifdef OSSL_USE_PROVIDER
163+
ossl_raise(eKDF, "EVP_KDF_derive");
164+
#else
98165
ossl_raise(eKDF, "PKCS5_PBKDF2_HMAC");
166+
#endif
99167
OPENSSL_cleanse(args.pass, passlen);
100168
ALLOCV_END(pass_tmp);
101169
ALLOCV_END(salt_tmp);

‎test/openssl/test_kdf.rb‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,27 @@ def test_pbkdf2_hmac_sha256_c_20000_len_32
5656
assert_equal(value1, value2)
5757
end
5858

59+
def test_pbkdf2_hmac_pkcs5
60+
# RFC 6070 c=1 parameters are below the SP 800-132 lower bounds
61+
p = "password"
62+
s = "salt"
63+
c = 1
64+
dk_len = 20
65+
expected = B(%w{ 0c 60 c8 0f 96 1f 0e 71 f3 a9 b5 24 af 60 12 06 2f e0 37 a6 })
66+
67+
# pkcs5: 0 enforces the lower bound checks
68+
if openssl?(3, 0, 0)
69+
assert_raise(OpenSSL::KDF::KDFError) {
70+
OpenSSL::KDF.pbkdf2_hmac(p, salt: s, iterations: c, length: dk_len, hash: "sha1", pkcs5: 0)
71+
}
72+
end
73+
74+
# pkcs5: 1 bypasses the lower bound checks, which is not FIPS approved
75+
omit_on_fips
76+
value = OpenSSL::KDF.pbkdf2_hmac(p, salt: s, iterations: c, length: dk_len, hash: "sha1", pkcs5: 1)
77+
assert_equal(expected, value)
78+
end
79+
5980
def test_scrypt_rfc7914_first
6081
pend "scrypt is not implemented" unless OpenSSL::KDF.respond_to?(:scrypt) # OpenSSL >= 1.1.0
6182
# scrypt is not available in FIPS.

0 commit comments

Comments
 (0)