]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
shared/crypto-util: add HKDF (RFC 5869) derivation helper
authorAnanth Bhaskararaman <antsub@gmail.com>
Tue, 2 Jun 2026 21:41:59 +0000 (03:11 +0530)
committerAnanth Bhaskararaman <antsub@gmail.com>
Tue, 21 Jul 2026 04:47:20 +0000 (10:17 +0530)
Mirrors the existing kdf_ss_derive / kdf_kb_hmac_derive helpers, wrapping
OpenSSL's "HKDF" EVP_KDF. Inputs and output are passed as struct iovec;
salt and info are optional (pass NULL or an empty iovec to omit). If
salt is omitted HKDF substitutes HashLen zero-bytes per RFC 5869.

The digest is a parameter now, so reimplement the existing SHA256-only
kdf_hkdf_sha256() as a thin wrapper around the new helper, keeping the
OpenSSL HKDF plumbing in one place.

Add test vectors against RFC 5869 Appendix A.1 and against the kernel's
fscrypt v2 master-key identifier construction (HKDF-SHA512 with empty
salt and info "fscrypt\x00\x01"), so future consumers can rely on the
helper matching that exact derivation.

src/shared/crypto-util.c
src/shared/crypto-util.h
src/test/test-crypto-util.c

index 55b28f213597a1a25044803628c5ebd096936034..b3053e7f1d1000061b8de3aa6f1fa861cb9d7919 100644 (file)
@@ -1343,10 +1343,13 @@ int kdf_argon2id_derive(
         return 0;
 }
 
-/* Perform HKDF-SHA256 derivation, producing derive_size bytes of output.
+/* Perform HKDF (RFC 5869). 'key' is the input keying material (required, non-empty). 'salt' and 'info'
+ * are optional: pass NULL or an empty iovec to omit. The output is written to 'ret' as a fresh allocation
+ * of 'derive_size' bytes (must be ≤ 255 × HashLen).
  *
- * For more details see: https://docs.openssl.org/master/man7/EVP_KDF-HKDF.html */
-int kdf_hkdf_sha256(
+ * For more details see: https://www.openssl.org/docs/manmaster/man7/EVP_KDF-HKDF.html */
+int kdf_hkdf_derive(
+                const char *digest,
                 const struct iovec *key,
                 const struct iovec *salt,
                 const struct iovec *info,
@@ -1355,9 +1358,10 @@ int kdf_hkdf_sha256(
 
         int r;
 
-        assert(!key || key->iov_len > 0);
-        assert(!salt || salt->iov_len > 0);
-        assert(!info || info->iov_len > 0);
+        assert(digest);
+        assert(iovec_is_set(key));
+        assert(iovec_is_valid(salt));
+        assert(iovec_is_valid(info));
         assert(derive_size > 0);
         assert(ret);
 
@@ -1365,9 +1369,9 @@ int kdf_hkdf_sha256(
         if (r < 0)
                 return r;
 
-        _cleanup_(EVP_KDF_freep) EVP_KDF *kdf = sym_EVP_KDF_fetch(/* propq= */ NULL, "HKDF", /* propq= */ NULL);
+        _cleanup_(EVP_KDF_freep) EVP_KDF *kdf = sym_EVP_KDF_fetch(/* libctx= */ NULL, "HKDF", /* properties= */ NULL);
         if (!kdf)
-                return log_openssl_errors(LOG_DEBUG, "Failed to create new EVP_KDF for HKDF");
+                return log_openssl_errors(LOG_DEBUG, "Failed to create new EVP_KDF");
 
         _cleanup_(EVP_KDF_CTX_freep) EVP_KDF_CTX *ctx = sym_EVP_KDF_CTX_new(kdf);
         if (!ctx)
@@ -1377,38 +1381,46 @@ int kdf_hkdf_sha256(
         if (!bld)
                 return log_openssl_errors(LOG_DEBUG, "Failed to create new OSSL_PARAM_BLD");
 
-        _cleanup_(erase_and_freep) void *buf = malloc(derive_size);
-        if (!buf)
-                return log_oom_debug();
+        if (!sym_OSSL_PARAM_BLD_push_utf8_string(bld, OSSL_KDF_PARAM_DIGEST, (char*) digest, /* bsize= */ 0))
+                return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF OSSL_KDF_PARAM_DIGEST");
 
-        if (!sym_OSSL_PARAM_BLD_push_utf8_string(bld, "digest", "SHA256", 0))
-                return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF digest");
+        if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, OSSL_KDF_PARAM_KEY, key->iov_base, key->iov_len))
+                return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF OSSL_KDF_PARAM_KEY");
 
-        if (key)
-                if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, "key", key->iov_base, key->iov_len))
-                        return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF key");
+        if (iovec_is_set(salt))
+                if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, OSSL_KDF_PARAM_SALT, salt->iov_base, salt->iov_len))
+                        return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF OSSL_KDF_PARAM_SALT");
 
-        if (salt)
-                if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, "salt", salt->iov_base, salt->iov_len))
-                        return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF salt");
-
-        if (info)
-                if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, "info", info->iov_base, info->iov_len))
-                        return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF info");
+        if (iovec_is_set(info))
+                if (!sym_OSSL_PARAM_BLD_push_octet_string(bld, OSSL_KDF_PARAM_INFO, info->iov_base, info->iov_len))
+                        return log_openssl_errors(LOG_DEBUG, "Failed to add HKDF OSSL_KDF_PARAM_INFO");
 
-        _cleanup_(OSSL_PARAM_freep) OSSL_PARAM *openssl_params = sym_OSSL_PARAM_BLD_to_param(bld);
-        if (!openssl_params)
+        _cleanup_(OSSL_PARAM_freep) OSSL_PARAM *params = sym_OSSL_PARAM_BLD_to_param(bld);
+        if (!params)
                 return log_openssl_errors(LOG_DEBUG, "Failed to build HKDF OSSL_PARAM");
 
-        if (sym_EVP_KDF_derive(ctx, buf, derive_size, openssl_params) <= 0)
-                return log_openssl_errors(LOG_DEBUG, "OpenSSL HKDF derive failed");
+        _cleanup_(erase_and_freep) void *buf = malloc(derive_size);
+        if (!buf)
+                return log_oom_debug();
 
-        ret->iov_base = TAKE_PTR(buf);
-        ret->iov_len = derive_size;
+        if (sym_EVP_KDF_derive(ctx, buf, derive_size, params) <= 0)
+                return log_openssl_errors(LOG_DEBUG, "OpenSSL HKDF derive failed");
 
+        *ret = IOVEC_MAKE(TAKE_PTR(buf), derive_size);
         return 0;
 }
 
+/* Perform HKDF-SHA256 derivation, producing derive_size bytes of output. */
+int kdf_hkdf_sha256(
+                const struct iovec *key,
+                const struct iovec *salt,
+                const struct iovec *info,
+                size_t derive_size,
+                struct iovec *ret) {
+
+        return kdf_hkdf_derive("SHA256", key, salt, info, derive_size, ret);
+}
+
 /* Encrypt the key data using RSA-OAEP with the provided label and specified digest algorithm. Returns 0 on
  * success, -EOPNOTSUPP if the digest algorithm is not supported, or < 0 for any other error. */
 int rsa_oaep_encrypt_bytes(
index b0ed8cc36b7b6cfed7550dd71a8e3ed1efa7d3e1..a49c39e50ecb53ae36a7ef3218748b492828ba3b 100644 (file)
@@ -392,6 +392,8 @@ int kdf_ss_derive(const char *digest, const void *key, size_t key_size, const vo
 
 int kdf_kb_hmac_derive(const char *mode, const char *digest, const void *key, size_t key_size, const void *salt, size_t salt_size, const void *info, size_t info_size, const void *seed, size_t seed_size, size_t derive_size, void **ret);
 
+int kdf_hkdf_derive(const char *digest, const struct iovec *key, const struct iovec *salt, const struct iovec *info, size_t derive_size, struct iovec *ret);
+
 int rsa_oaep_encrypt_bytes(const EVP_PKEY *pkey, const char *digest_alg, const char *label, const void *decrypted_key, size_t decrypted_key_size, void **ret_encrypt_key, size_t *ret_encrypt_key_size);
 
 int rsa_pkey_to_suitable_key_size(EVP_PKEY *pkey, size_t *ret_suitable_key_size);
index 528482eb9d647edbfdf8323d4117a920c07e77b1..b3ae91b0d8b0a4bf2eed9666f84338fd9c7565ab 100644 (file)
@@ -342,6 +342,106 @@ TEST(kdf_ss_derive) {
                 "30EB1A1E9DEA7DE4DDB8F3FDF50A01E30581D606C1228D98AFF691DF743AC2EE9D99EFD2AE1946C079AA18C9524877FA65D5065F0DAED058AB3416AF80EB2B73");
 }
 
+static void check_hkdf_derive(
+                const char *digest,
+                const char *hex_key,
+                const char *hex_salt,
+                const char *hex_info,
+                const char *hex_expected) {
+
+        DEFINE_HEX_PTR(key, hex_key);
+        DEFINE_HEX_PTR(salt, hex_salt);
+        DEFINE_HEX_PTR(info, hex_info);
+        DEFINE_HEX_PTR(expected, hex_expected);
+
+        _cleanup_(iovec_done) struct iovec derived = {};
+        assert_se(kdf_hkdf_derive(
+                        digest,
+                        &IOVEC_MAKE(key, key_len),
+                        &IOVEC_MAKE(salt, salt_len),
+                        &IOVEC_MAKE(info, info_len),
+                        expected_len,
+                        &derived) >= 0);
+        assert_se(memcmp_nn(derived.iov_base, derived.iov_len, expected, expected_len) == 0);
+}
+
+TEST(kdf_hkdf_derive) {
+        /* RFC 5869, Appendix A.1: basic HKDF-SHA256 test vector. */
+        check_hkdf_derive(
+                "SHA256",
+                "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
+                "000102030405060708090a0b0c",
+                "f0f1f2f3f4f5f6f7f8f9",
+                "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865");
+
+        /* fscrypt v2 master-key identifier derivation. The Linux kernel uses standard HKDF-SHA512
+         * (RFC 5869) with empty salt (i.e. HashLen=64 zero bytes per RFC 5869 §2.2), but splits the
+         * info string for code organisation: fs/crypto/hkdf.c's fscrypt_hkdf_expand() hard-codes an
+         * 8-byte "fscrypt\0" prefix, then appends a context byte and the per-caller info argument
+         * before HKDF-Expand. For the key identifier the caller passes context=0x01
+         * (HKDF_CONTEXT_KEY_IDENTIFIER) with empty info, so the effective standard-HKDF info is the
+         * 9-byte string "fscrypt\0\x01". The vectors below assert that against representative IKM
+         * sizes (all-zeros, full 64-byte master key, AES-256 sized, and an arbitrary 64-byte
+         * payload). */
+        const char *fscrypt_info_hex = "667363727970740001";
+
+        check_hkdf_derive(
+                "SHA512",
+                "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+                /* hex_salt= */ NULL,
+                fscrypt_info_hex,
+                "69d7f347a3ca7bfa3e0c1d84e476d050");
+
+        check_hkdf_derive(
+                "SHA512",
+                "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f",
+                /* hex_salt= */ NULL,
+                fscrypt_info_hex,
+                "8699c2c53707405da5aba5ae4d8583c0");
+
+        check_hkdf_derive(
+                "SHA512",
+                "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+                /* hex_salt= */ NULL,
+                fscrypt_info_hex,
+                "37d7d76a59400083289c185526730d34");
+
+        check_hkdf_derive(
+                "SHA512",
+                "73797374656d642d686f6d6564207632207465737420766563746f72206b65790000000000000000000000000000000000000000000000000000000000000000",
+                /* hex_salt= */ NULL,
+                fscrypt_info_hex,
+                "ab8550968fca25b08222de0ffb7b2986");
+
+        /* Exercise the salt=NULL path in kdf_hkdf_derive directly. DEFINE_HEX_PTR(NULL) allocates a
+         * 1-byte buffer of size 0, so check_hkdf_derive() above feeds the function an
+         * iovec_is_valid()-but-not-iovec_is_set() iovec. Passing salt=NULL outright takes the other
+         * branch. Per RFC 5869 §2.2 an absent salt is identical to HashLen zero bytes, which is also
+         * what OpenSSL substitutes for an empty-octet-string salt, so the derived output must match
+         * byte-for-byte against the third vector above. */
+        {
+                static const uint8_t ikm[] = {
+                        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
+                        0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
+                        0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+                        0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+                };
+                static const uint8_t info[] = {
+                        'f', 's', 'c', 'r', 'y', 'p', 't', 0x00, 0x01,
+                };
+                DEFINE_HEX_PTR(expected, "37d7d76a59400083289c185526730d34");
+                _cleanup_(iovec_done) struct iovec derived = {};
+
+                assert_se(kdf_hkdf_derive(
+                                "SHA512",
+                                &IOVEC_MAKE((void*) ikm, sizeof(ikm)),
+                                /* salt= */ NULL,
+                                &IOVEC_MAKE((void*) info, sizeof(info)),
+                                expected_len, &derived) >= 0);
+                assert_se(memcmp_nn(derived.iov_base, derived.iov_len, expected, expected_len) == 0);
+        }
+}
+
 static void check_cipher(
                 const char *alg,
                 size_t bits,