]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
add quic header protection to isc_crypto
authorAydın Mercan <aydin@isc.org>
Thu, 12 Feb 2026 06:08:56 +0000 (09:08 +0300)
committerAydın Mercan <aydin@isc.org>
Tue, 14 Jul 2026 13:40:49 +0000 (16:40 +0300)
QUIC uses a custom PRF construction to protect parts of the packet
header. This PRF is derived from the negotiated AEAD key and uses
unauthenticated encryption internally.

We do not expose the primitives underneath (AES-ECB and ChaCha20) as
they shouldn't be within the reach of contributors for their own safety.
Allowing such functionality to be used easily can only result it
problems not to dissimilar to leaving a baby with open bottles of
cleaning supplies.

lib/isc/crypto/ossl1_1.c
lib/isc/crypto/ossl3.c
lib/isc/include/isc/crypto.h
meson.build
tests/isc/crypto_test.c

index 31d344b9961479f0a345d349af992f792384f103..ac9d876d3fe7f6a13eaa58f9844488c73b51ae33 100644 (file)
 
 #include <stdint.h>
 
+#include <openssl/aes.h>
 #include <openssl/crypto.h>
 #include <openssl/err.h>
 #include <openssl/evp.h>
 #include <openssl/kdf.h>
+#include <openssl/opensslv.h>
 #include <openssl/rand.h>
 #include <openssl/ssl.h>
 
 #include <isc/buffer.h>
 #include <isc/crypto.h>
+#include <isc/endian.h>
 #include <isc/hmac.h>
 #include <isc/log.h>
 #include <isc/magic.h>
 #include <openssl/aead.h>
 #endif /* HAVE_OPENSSL_AEAD_H */
 
+#if HAVE_CRYPTO_CHACHA_20
+#include <openssl/chacha.h>
+#endif /* HAVE_CRYPTO_CHACHA_20 */
+
 #ifdef HAVE_OPENSSL_HKDF_H
 #include <openssl/hkdf.h>
 #endif /* HAVE_OPENSSL_HKDF_H */
@@ -56,6 +63,20 @@ struct isc_hmac_key {
        uint8_t secret[];
 };
 
+struct isc_crypto_quic_hp_protect {
+       uint32_t magic;
+       isc_crypto_quic_hp_protect_algorithm_t algorithm;
+       isc_mem_t *mctx;
+       union {
+               AES_KEY aes;
+#if HAVE_CRYPTO_CHACHA_20
+               uint8_t chacha20[32];
+#else  /* HAVE_CRYPTO_CHACHA_20 */
+               EVP_CIPHER_CTX *chacha20;
+#endif /* HAVE_CRYPTO_CHACHA_20 */
+       } key;
+};
+
 #ifdef HAVE_EVP_AEAD_CTX_NEW
 STATIC_ASSERT(ISC_TYPES_COMPATIBLE(isc_crypto_aead_t, EVP_AEAD_CTX),
              "isc_crypto_aead_t is not compatible with EVP_AEAD_CTX");
@@ -64,6 +85,8 @@ STATIC_ASSERT(ISC_TYPES_COMPATIBLE(isc_crypto_aead_t, EVP_CIPHER_CTX),
              "isc_crypto_aead_t is not compatible with EVP_CIPHER_CTX");
 #endif /* HAVE_EVP_AEAD_CTX_NEW */
 
+constexpr uint32_t crypto_quic_hp_protect_magic = ISC_MAGIC('C', 'Q', 'h', 'p');
+
 static isc_mem_t *isc__crypto_mctx = NULL;
 
 #define md_register_algorithm(alg, upperalg)                              \
@@ -1014,6 +1037,164 @@ cleanup:
 
 #endif /* HAVE_OPENSSL_HKDF_H */
 
+void
+isc_crypto_quic_hp_protect_destroy(isc_crypto_quic_hp_protect_t **protp) {
+       isc_crypto_quic_hp_protect_t *prot;
+
+       REQUIRE(protp != NULL && *protp != NULL &&
+               (*protp)->magic == crypto_quic_hp_protect_magic);
+
+       prot = MOVE_OWNERSHIP(*protp);
+
+       prot->magic = 0x00;
+
+#if !HAVE_CRYPTO_CHACHA_20
+       if (prot->algorithm == ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_CHACHA20) {
+               EVP_CIPHER_CTX_free(prot->key.chacha20);
+       }
+#endif /* HAVE_CRYPTO_CHACHA_20 */
+
+       isc_safe_memwipe(&prot->key, ISC_MAX(sizeof(prot->key.aes),
+                                            sizeof(prot->key.chacha20)));
+
+       isc_mem_putanddetach(&prot->mctx, prot, sizeof(*prot));
+}
+
+isc_result_t
+isc_crypto_quic_hp_protect_create(
+       isc_mem_t *mctx, isc_constregion_t key,
+       isc_crypto_quic_hp_protect_algorithm_t algorithm,
+       isc_crypto_quic_hp_protect_t **protp) {
+       isc_crypto_quic_hp_protect_t *prot;
+       isc_result_t result;
+
+       REQUIRE(protp != NULL && *protp == NULL);
+       REQUIRE(key.base != NULL);
+
+       prot = isc_mem_get(mctx, sizeof(*prot));
+       *prot = (isc_crypto_quic_hp_protect_t){
+               .magic = crypto_quic_hp_protect_magic,
+               .algorithm = algorithm,
+               .mctx = isc_mem_ref(mctx),
+       };
+
+       switch (algorithm) {
+       case ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_AES128:
+               INSIST(key.length == isc_crypto_aes128gcm_key_length);
+               if (AES_set_encrypt_key(key.base, 128, &prot->key.aes) != 0) {
+                       CLEANUP(CRYPTO_ERROR("AES_set_encrypt_key"));
+               }
+               break;
+       case ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_AES256:
+               INSIST(key.length == isc_crypto_aes256gcm_key_length);
+               if (AES_set_encrypt_key(key.base, 256, &prot->key.aes) != 0) {
+                       CLEANUP(CRYPTO_ERROR("AES_set_encrypt_key"));
+               }
+               break;
+       case ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_CHACHA20:
+               INSIST(key.length == isc_crypto_chacha20poly1305_key_length);
+               if (isc_crypto_fips_mode()) {
+                       CLEANUP(ISC_R_NOTIMPLEMENTED);
+               }
+#if HAVE_CRYPTO_CHACHA_20
+               memmove(prot->key.chacha20, key.base,
+                       isc_crypto_chacha20poly1305_key_length);
+#else  /* HAVE_CRYPTO_CHACHA_20 */
+               prot->key.chacha20 = EVP_CIPHER_CTX_new();
+               if (prot->key.chacha20 == NULL) {
+                       CLEANUP(CRYPTO_ERROR("EVP_CIPHER_CTX_new"));
+               }
+
+               if (EVP_EncryptInit_ex(prot->key.chacha20, EVP_chacha20(), NULL,
+                                      key.base, NULL) != 1)
+               {
+                       EVP_CIPHER_CTX_free(prot->key.chacha20);
+                       CLEANUP(CRYPTO_ERROR("EVP_EncryptInit_ex"));
+               }
+#endif /* HAVE_CRYPTO_CHACHA_20 */
+               break;
+       default:
+               UNREACHABLE();
+       }
+
+       *protp = prot;
+
+       return ISC_R_SUCCESS;
+
+cleanup:
+       isc_mem_putanddetach(&prot->mctx, prot, sizeof(*prot));
+       return result;
+}
+
+isc_result_t
+isc_crypto_quic_hp_protect_mask(isc_crypto_quic_hp_protect_t *prot,
+                               uint8_t *out, const uint8_t *sample) {
+       static const uint8_t zeros[5] = { 0x00, 0x00, 0x00, 0x00, 0x00 };
+#if HAVE_CRYPTO_CHACHA_20
+       const uint8_t *nonce;
+       uint64_t counter;
+#else  /* HAVE_CRYPTO_CHACHA_20 */
+       int len;
+#endif /* HAVE_CRYPTO_CHACHA_20 */
+
+       REQUIRE(prot != NULL && prot->magic == crypto_quic_hp_protect_magic);
+       REQUIRE(out != NULL && sample != NULL);
+
+       switch (prot->algorithm) {
+       case ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_AES128:
+       case ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_AES256:
+               AES_ecb_encrypt(sample, out, &prot->key.aes, 1);
+               break;
+       case ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_CHACHA20:
+#if HAVE_CRYPTO_CHACHA_20
+               /*
+                * `CRYPTO_chacha_20` is the IETF variant of ChaCha20 with a
+                * 32-bit counter and a 96-bit nonce in BoringSSL and its forks
+                * such as AWS-LC.
+                *
+                * However, LibreSSL implements the original paper with 64-bit
+                * counter and a 64-bit nonce. This can easily be turned into
+                * the IETF variant by reading 32-bits more to the counter from
+                * the sample and using the rest and the nonce. The ChaCha20
+                * block state will be equivalent.
+                */
+#ifdef LIBRESSL_VERSION_NUMBER
+               counter = ISC_U8TO64_LE(sample);
+               nonce = sample + 8;
+#else  /* LIBRESSL_VERSION_NUMBER */
+               counter = ISC_U8TO32_LE(sample);
+               nonce = sample + 4;
+#endif /* LIBRESSL_VERSION_NUMBER */
+               CRYPTO_chacha_20(out, zeros, sizeof(zeros), prot->key.chacha20,
+                                nonce, counter);
+
+#else  /* HAVE_CRYPTO_CHACHA_20 */
+               if (EVP_EncryptInit_ex(prot->key.chacha20, NULL, NULL, NULL,
+                                      sample) != 1)
+               {
+                       return CRYPTO_ERROR("EVP_EncryptInit_ex");
+               }
+
+               if (EVP_EncryptUpdate(prot->key.chacha20, out, &len, zeros,
+                                     sizeof(zeros)) != 1)
+               {
+                       return CRYPTO_ERROR("EVP_EncryptUpdate");
+               }
+
+               if (EVP_EncryptFinal_ex(prot->key.chacha20, out + sizeof(zeros),
+                                       &len) != 1)
+               {
+                       return CRYPTO_ERROR("EVP_EncryptFinal_ex");
+               }
+#endif /* HAVE_CRYPTO_CHACHA_20 */
+               break;
+       default:
+               UNREACHABLE();
+       }
+
+       return ISC_R_SUCCESS;
+}
+
 #ifdef HAVE_FIPS_MODE
 bool
 isc_crypto_fips_mode(void) {
index 0ce8752483396af39092d805bfae249be66e929e..c05653517b005486f2dd87d858314c79af34024a 100644 (file)
@@ -49,9 +49,16 @@ struct isc_hmac_key {
        uint8_t secret[];
 };
 
+struct isc_crypto_quic_hp_protect {
+       uint32_t magic;
+       isc_mem_t *mctx;
+       EVP_CIPHER_CTX *ctx;
+};
+
 STATIC_ASSERT(ISC_TYPES_COMPATIBLE(isc_crypto_aead_t, EVP_CIPHER_CTX),
              "isc_crypto_aead_t is not compatible with EVP_CIPHER_CTX");
 
+constexpr uint32_t quic_hp_protect_magic = ISC_MAGIC('C', 'Q', 'h', 'p');
 constexpr uint32_t hmac_key_magic = ISC_MAGIC('H', 'M', 'A', 'C');
 
 static OSSL_PROVIDER *base = NULL, *fips = NULL;
@@ -70,6 +77,11 @@ static EVP_CIPHER *evp_aes_128_gcm = NULL;
 static EVP_CIPHER *evp_aes_256_gcm = NULL;
 static EVP_CIPHER *evp_chacha20poly1305 = NULL;
 
+/* QUIC Header Protection */
+static EVP_CIPHER *evp_aes_128_ctr = NULL;
+static EVP_CIPHER *evp_aes_256_ctr = NULL;
+static EVP_CIPHER *evp_chacha20 = NULL;
+
 static isc_constregion_t md_to_name[ISC_MD_MAX] = {
        [ISC_MD_UNKNOWN] = { NULL, 0 },
        [ISC_MD_MD5] = { "MD5", sizeof("MD5") - 1 },
@@ -125,6 +137,9 @@ register_algorithms(void) {
                INSIST(evp_chacha20poly1305 == NULL);
                evp_chacha20poly1305 =
                        EVP_CIPHER_fetch(NULL, "ChaCha20-Poly1305", NULL);
+
+               INSIST(evp_chacha20 == NULL);
+               evp_chacha20 = EVP_CIPHER_fetch(NULL, "ChaCha20", NULL);
        }
 
        md_register_algorithm(SHA1);
@@ -139,6 +154,12 @@ register_algorithms(void) {
        INSIST(evp_aes_256_gcm == NULL);
        evp_aes_256_gcm = EVP_CIPHER_fetch(NULL, "AES-256-GCM", NULL);
 
+       INSIST(evp_aes_128_ctr == NULL);
+       evp_aes_128_ctr = EVP_CIPHER_fetch(NULL, "AES-128-CTR", NULL);
+
+       INSIST(evp_aes_256_ctr == NULL);
+       evp_aes_256_ctr = EVP_CIPHER_fetch(NULL, "AES-256-CTR", NULL);
+
        /* We _must_ have HMAC */
        evp_hmac = EVP_MAC_fetch(NULL, "HMAC", NULL);
        if (evp_hmac == NULL) {
@@ -183,6 +204,15 @@ unregister_algorithms(void) {
        EVP_MAC_free(evp_hmac);
        evp_hmac = NULL;
 
+       EVP_CIPHER_free(evp_chacha20);
+       evp_chacha20 = NULL;
+
+       EVP_CIPHER_free(evp_aes_256_ctr);
+       evp_aes_256_ctr = NULL;
+
+       EVP_CIPHER_free(evp_aes_128_ctr);
+       evp_aes_128_ctr = NULL;
+
        EVP_CIPHER_free(evp_chacha20poly1305);
        evp_chacha20poly1305 = NULL;
 
@@ -776,6 +806,106 @@ cleanup:
        return result;
 }
 
+void
+isc_crypto_quic_hp_protect_destroy(isc_crypto_quic_hp_protect_t **protp) {
+       isc_crypto_quic_hp_protect_t *prot;
+
+       REQUIRE(protp != NULL && *protp != NULL &&
+               (*protp)->magic == quic_hp_protect_magic);
+
+       prot = MOVE_OWNERSHIP(*protp);
+       prot->magic = 0x00;
+       EVP_CIPHER_CTX_free(prot->ctx);
+       isc_mem_putanddetach(&prot->mctx, prot, sizeof(*prot));
+}
+
+isc_result_t
+isc_crypto_quic_hp_protect_create(
+       isc_mem_t *mctx, isc_constregion_t key,
+       isc_crypto_quic_hp_protect_algorithm_t algorithm,
+       isc_crypto_quic_hp_protect_t **protp) {
+       isc_crypto_quic_hp_protect_t *prot;
+       const EVP_CIPHER *evp;
+       EVP_CIPHER_CTX *ctx;
+       size_t len;
+
+       REQUIRE(protp != NULL && *protp == NULL);
+       REQUIRE(key.base != NULL);
+
+       switch (algorithm) {
+       case ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_AES128:
+               len = isc_crypto_aes128gcm_key_length;
+               evp = evp_aes_128_ctr;
+               break;
+       case ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_AES256:
+               len = isc_crypto_aes256gcm_key_length;
+               evp = evp_aes_256_ctr;
+               break;
+       case ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_CHACHA20:
+               len = isc_crypto_chacha20poly1305_key_length;
+               evp = evp_chacha20;
+               break;
+       default:
+               UNREACHABLE();
+       }
+
+       INSIST(key.length == len);
+
+       if (evp == NULL) {
+               return ISC_R_NOTIMPLEMENTED;
+       }
+
+       ctx = EVP_CIPHER_CTX_new();
+       if (ctx == NULL) {
+               return CRYPTO_ERROR("EVP_CIPHER_CTX_new");
+       }
+
+       if (EVP_CipherInit_ex2(ctx, evp, key.base, NULL, 1, NULL) != 1) {
+               EVP_CIPHER_CTX_free(ctx);
+               return CRYPTO_ERROR("EVP_CipherInit_ex2");
+       }
+
+       prot = isc_mem_get(mctx, sizeof(*prot));
+       *prot = (isc_crypto_quic_hp_protect_t){
+               .magic = quic_hp_protect_magic,
+               .ctx = ctx,
+       };
+       isc_mem_attach(mctx, &prot->mctx);
+
+       *protp = prot;
+
+       return ISC_R_SUCCESS;
+}
+
+isc_result_t
+isc_crypto_quic_hp_protect_mask(isc_crypto_quic_hp_protect_t *prot,
+                               uint8_t *out, const uint8_t *sample) {
+       static const uint8_t zeros[5] = { 0x00, 0x00, 0x00, 0x00, 0x00 };
+       isc_result_t result;
+       int len;
+
+       REQUIRE(prot != NULL && prot->magic == quic_hp_protect_magic);
+       REQUIRE(out != NULL && sample != NULL);
+
+       if (EVP_EncryptInit_ex2(prot->ctx, NULL, NULL, sample, NULL) != 1) {
+               CLEANUP(CRYPTO_ERROR("EVP_EncryptInit_ex2"));
+       }
+
+       if (EVP_EncryptUpdate(prot->ctx, out, &len, zeros, sizeof(zeros)) != 1)
+       {
+               CLEANUP(CRYPTO_ERROR("EVP_EncryptUpdate"));
+       }
+
+       if (EVP_EncryptFinal_ex(prot->ctx, out + sizeof(zeros), &len) != 1) {
+               CLEANUP(CRYPTO_ERROR("EVP_EncryptFinal_ex"));
+       }
+
+       result = ISC_R_SUCCESS;
+
+cleanup:
+       return result;
+}
+
 bool
 isc_crypto_fips_mode(void) {
        return EVP_default_properties_is_fips_enabled(NULL) != 0;
index 08cd0c82fa99306429bde388243addfbc6f636f7..0d64f1cb1dde7af955107cfb04e0242062c5467a 100644 (file)
@@ -33,6 +33,15 @@ typedef struct evp_aead_ctx_st isc_crypto_aead_t;
 typedef struct evp_cipher_ctx_st isc_crypto_aead_t;
 #endif /* HAVE_EVP_AEAD_CTX_NEW */
 
+/**
+ * \brief
+ * Context to a mask generator to protect QUIC packet headers as specified by
+ * RFC 9001, Section 5.4. [1]
+ *
+ * [1]: https://datatracker.ietf.org/doc/html/rfc9001#section-5.4
+ */
+typedef struct isc_crypto_quic_hp_protect isc_crypto_quic_hp_protect_t;
+
 typedef enum isc_crypto_aead_algorithm {
        ISC_CRYPTO_AEAD_ALGORITHM_INVALID = 0,
        ISC_CRYPTO_AEAD_ALGORITHM_AES128GCM = 1,
@@ -48,6 +57,14 @@ typedef enum isc_crypto_aead_direction {
        ISC_CRYPTO_AEAD_DIRECTION_MAX = 3,
 } isc_crypto_aead_direction_t;
 
+typedef enum isc_crypto_quic_hp_protect_algorithm {
+       ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_INVALID = 0,
+       ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_AES128 = 1,
+       ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_AES256 = 2,
+       ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_CHACHA20 = 3,
+       ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_MAX = 4,
+} isc_crypto_quic_hp_protect_algorithm_t;
+
 constexpr size_t isc_crypto_aes128gcm_key_length = 16;
 constexpr size_t isc_crypto_aes256gcm_key_length = 32;
 constexpr size_t isc_crypto_chacha20poly1305_key_length = 32;
@@ -212,6 +229,52 @@ isc_crypto_hkdf(isc_region_t out, isc_md_type_t md, isc_constregion_t secret,
  * \retval ISC_R_CRYPTOFAILURE on libcrypto failure
  */
 
+void
+isc_crypto_quic_hp_protect_destroy(isc_crypto_quic_hp_protect_t **protp);
+/**<
+ * \brief
+ * Destroy a QUIC header protection context.
+ *
+ * Requires:
+ * - `*protp` is a valid QUIC header protection context.
+ */
+
+isc_result_t
+isc_crypto_quic_hp_protect_create(
+       isc_mem_t *mctx, isc_constregion_t key,
+       isc_crypto_quic_hp_protect_algorithm_t algorithm,
+       isc_crypto_quic_hp_protect_t         **protp);
+/**<
+ * \brief
+ * Create a new QUIC header protection context.
+ *
+ * Requires:
+ * - `mctx` is a valid memory context
+ * - `key` is a valid AEAD-key that has the correct key size for
+ * `aead_algorithm`.
+ * - `aead_algorithm` is a valid AEAD type.
+ * - `protp != NULL` and `*protp == NULL`
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_CRYPTOFAILURE on libcrypto failure
+ */
+
+isc_result_t
+isc_crypto_quic_hp_protect_mask(isc_crypto_quic_hp_protect_t *prot,
+                               uint8_t *out, const uint8_t *sample);
+/**<
+ * \brief
+ * Generate a QUIC header protection mask.
+ *
+ * Requires:
+ * - `prot` is a valid QUIC header protection context.
+ * - `out` is non-`NULL` and points to 16 bytes.
+ * - `sample` is non-NULL and points to 16 bytes of sampleable data.
+ *
+ * \retval ISC_R_SUCCESS on success
+ * \retval ISC_R_CRYPTOFAILURE on libcrypto failure
+ */
+
 bool
 isc_crypto_fips_mode(void);
 /*
index 6e02fda5a64839b9442130db36823be7e177fc7d..4852db29dfc014fe2472ca0f91897490fa1a9149 100644 (file)
@@ -684,6 +684,7 @@ foreach fn, header : {
     'BIO_write_ex': '#include <openssl/bio.h>',
     'EVP_PKEY_eq': '#include <openssl/evp.h>',
     'SSL_CTX_set1_cert_store': '#include <openssl/ssl.h>',
+    'CRYPTO_chacha_20': '#include <openssl/chacha.h>',
 }
     config.set10(
         'HAVE_@0@'.format(fn.to_upper()),
index 10ba76016146a66001a0af8fffbed1b922293989..7ab6886aa85f6c5e3b8cbe4d4e713036ff044626 100644 (file)
@@ -44,6 +44,7 @@
 #include <tests/isc.h>
 
 typedef struct aead_testcase aead_testcase_t;
+typedef struct quic_hp_testcase quic_hp_testcase_t;
 
 struct aead_testcase {
        isc_crypto_aead_algorithm_t algorithm;
@@ -54,6 +55,13 @@ struct aead_testcase {
        const uint8_t additional_data[8];
 };
 
+struct quic_hp_testcase {
+       isc_crypto_quic_hp_protect_algorithm_t algorithm;
+       size_t keylen;
+       const uint8_t mask[5];
+       const uint8_t key[32];
+       const uint8_t sample[16];
+};
 
 static void
 test_aead(const aead_testcase_t *const testcase) {
@@ -283,10 +291,88 @@ ISC_RUN_TEST_IMPL(hkdf_expand_label) {
        assert_memory_equal(expected, actual, 32);
 }
 
+ISC_RUN_TEST_IMPL(quic_hp_protect) {
+       isc_crypto_quic_hp_protect_t *prot = NULL;
+       isc_constregion_t key;
+       uint8_t generated[16];
+       isc_result_t result;
+       size_t i;
+
+       static const quic_hp_testcase_t testcases[] = {
+               {
+                       ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_AES128,
+                       isc_crypto_aes128gcm_key_length,
+                       { 0x2e, 0xc0, 0xd8, 0x35, 0x6a },
+                       { 0xc2, 0x06, 0xb8, 0xd9, 0xb9, 0xf0, 0xf3, 0x76, 0x44,
+                         0x43, 0x0b, 0x49, 0x0e, 0xea, 0xa3, 0x14 },
+                       { 0x2c, 0xd0, 0x99, 0x1c, 0xd2, 0x5b, 0x0a, 0xac, 0x40,
+                         0x6a, 0x58, 0x16, 0xb6, 0x39, 0x41, 0x00 },
+               },
+
+       };
+
+       static const quic_hp_testcase_t non_fips_testcases[] = {
+               {
+                       ISC_CRYPTO_QUIC_HP_PROTECT_ALGORITHM_CHACHA20,
+                       isc_crypto_chacha20poly1305_key_length,
+                       { 0xae, 0xfe, 0xfe, 0x7d, 0x03 },
+                       { 0x25, 0xa2, 0x82, 0xb9, 0xe8, 0x2f, 0x06, 0xf2,
+                         0x1f, 0x48, 0x89, 0x17, 0xa4, 0xfc, 0x8f, 0x1b,
+                         0x73, 0x57, 0x36, 0x85, 0x60, 0x85, 0x97, 0xd0,
+                         0xef, 0xcb, 0x07, 0x6b, 0x0a, 0xb7, 0xa7, 0xa4 },
+                       { 0x5e, 0x5c, 0xd5, 0x5c, 0x41, 0xf6, 0x90, 0x80, 0x57,
+                         0x5d, 0x79, 0x99, 0xc2, 0x5a, 0x5b, 0xfb },
+               },
+       };
+
+       for (i = 0; i < ARRAY_SIZE(testcases); i++) {
+               key = (isc_constregion_t){
+                       .base = testcases[i].key,
+                       .length = testcases[i].keylen,
+               };
+
+               result = isc_crypto_quic_hp_protect_create(
+                       isc_g_mctx, key, testcases[i].algorithm, &prot);
+               assert_int_equal(result, ISC_R_SUCCESS);
+
+               result = isc_crypto_quic_hp_protect_mask(prot, generated,
+                                                        testcases[i].sample);
+               assert_int_equal(result, ISC_R_SUCCESS);
+               assert_memory_equal(generated, testcases[i].mask,
+                                   sizeof(testcases[i].mask));
+
+               isc_crypto_quic_hp_protect_destroy(&prot);
+       }
+
+       if (isc_crypto_fips_mode()) {
+               return;
+       }
+
+       for (i = 0; i < ARRAY_SIZE(non_fips_testcases); i++) {
+               key = (isc_constregion_t){
+                       .base = testcases[i].key,
+                       .length = testcases[i].keylen,
+               };
+
+               result = isc_crypto_quic_hp_protect_create(
+                       isc_g_mctx, key, testcases[i].algorithm, &prot);
+               assert_int_equal(result, ISC_R_SUCCESS);
+
+               result = isc_crypto_quic_hp_protect_mask(prot, generated,
+                                                        testcases[i].sample);
+               assert_int_equal(result, ISC_R_SUCCESS);
+               assert_memory_equal(generated, testcases[i].mask,
+                                   sizeof(testcases[i].mask));
+
+               isc_crypto_quic_hp_protect_destroy(&prot);
+       }
+}
+
 ISC_TEST_LIST_START
 ISC_TEST_ENTRY(aead)
 ISC_TEST_ENTRY(hkdf)
 ISC_TEST_ENTRY(hkdf_expand_label)
+ISC_TEST_ENTRY(quic_hp_protect)
 ISC_TEST_LIST_END
 
 ISC_TEST_MAIN