[Git][NTPsec/ntpsec][2026-06-drop-libaes-siv] fix: handle empty plaintext in AES-SIV wrapper
Mark Atwood (@MarkAtwood)
gitlab at mg.gitlab.com
Sat Oct 3 01:42:53 UTC 2026
Mark Atwood pushed to branch 2026-06-drop-libaes-siv at NTPsec / ntpsec
Commits:
7735674e by Mark Atwood at 2026-10-02T18:42:48-07:00
fix: handle empty plaintext in AES-SIV wrapper
OpenSSL before 3.5.0 cannot produce an AES-SIV tag for an empty
plaintext: the SIV provider returns early on zero-length input and
the final step fails. NTS authenticates client packets with an empty
plaintext, so extens_client_send() hit exit(1). Compute that tag
directly with RFC 5297 S2V over AES-CMAC, and verify tag-only input
with CRYPTO_memcmp(). Remove once supported distributions ship
OpenSSL >= 3.5.0.
Copy the received tag into a local buffer instead of casting away
const for EVP_CTRL_AEAD_SET_TAG, which broke -Werror=cast-qual builds.
Build aes_siv_evp.c only when NTS is enabled. The OpenSSL >= 3.0
check is NTS-only, so --disable-nts builds against older OpenSSL
failed to compile it.
Verified bit-exact against the removed libaes_siv for 32/48/64-byte
keys and plaintext lengths 0-64 on OpenSSL 3.0.13, 3.3.7 and 3.5.9.
- - - - -
2 changed files:
- libntp/aes_siv_evp.c
- libntp/wscript
Changes:
=====================================
libntp/aes_siv_evp.c
=====================================
@@ -20,6 +20,7 @@
#include <string.h>
#include <openssl/evp.h>
+#include <openssl/core_names.h>
#include <openssl/crypto.h>
#include <openssl/err.h>
@@ -89,6 +90,125 @@ static const char *get_cipher_name(size_t key_len) {
}
}
+/*
+ * OpenSSL before 3.5.0 cannot produce an AES-SIV tag for an empty
+ * plaintext: its SIV provider returns early on zero-length input, so
+ * the final step fails. NTS authenticates packets with an empty
+ * plaintext, so for that case we compute the tag ourselves with the
+ * RFC 5297 S2V construction over AES-CMAC. The ciphertext is empty,
+ * so the tag is the whole output.
+ *
+ * Remove this once the distributions we support ship OpenSSL >= 3.5.0
+ * (Debian oldstable and Ubuntu 24.04 LTS were still on 3.0.x in 2026).
+ */
+
+/* RFC 5297 doubling in GF(2^128) */
+static void siv_dbl(unsigned char block[SIV_TAG_LEN]) {
+ unsigned char carry = block[0] >> 7;
+ int i;
+
+ for (i = 0; i < SIV_TAG_LEN - 1; i++) {
+ block[i] = (unsigned char)((block[i] << 1) | (block[i + 1] >> 7));
+ }
+ block[SIV_TAG_LEN - 1] = (unsigned char)((block[SIV_TAG_LEN - 1] << 1) ^
+ (carry * 0x87));
+}
+
+static int siv_cmac(EVP_MAC_CTX *mctx, const unsigned char *key, size_t key_len,
+ const OSSL_PARAM *params,
+ const unsigned char *in, size_t in_len,
+ unsigned char out[SIV_TAG_LEN]) {
+ size_t out_len;
+
+ return EVP_MAC_init(mctx, key, key_len, params) == 1 &&
+ EVP_MAC_update(mctx, in, in_len) == 1 &&
+ EVP_MAC_final(mctx, out, &out_len, SIV_TAG_LEN) == 1;
+}
+
+/*
+ * S2V(K1, ad, nonce, "") with K1 the first half of the SIV key.
+ * Components are included under the same rules AES_SIV_Encrypt()
+ * uses when it feeds them to OpenSSL.
+ */
+static int siv_empty_tag(unsigned char tag[SIV_TAG_LEN],
+ unsigned char const *key, size_t key_len,
+ unsigned char const *nonce, size_t nonce_len,
+ unsigned char const *ad, size_t ad_len) {
+ static const unsigned char zero[SIV_TAG_LEN];
+ static char cbc128[] = "AES-128-CBC";
+ static char cbc192[] = "AES-192-CBC";
+ static char cbc256[] = "AES-256-CBC";
+ char *cbc_name;
+ EVP_MAC *mac;
+ EVP_MAC_CTX *mctx = NULL;
+ OSSL_PARAM params[2];
+ unsigned char d[SIV_TAG_LEN], t[SIV_TAG_LEN];
+ size_t k1_len = key_len / 2;
+ int ret = 0;
+ int i;
+
+ switch (key_len) {
+ case 32:
+ cbc_name = cbc128;
+ break;
+ case 48:
+ cbc_name = cbc192;
+ break;
+ case 64:
+ cbc_name = cbc256;
+ break;
+ default:
+ return 0;
+ }
+
+ mac = EVP_MAC_fetch(NULL, "CMAC", NULL);
+ if (mac == NULL) {
+ return 0;
+ }
+ mctx = EVP_MAC_CTX_new(mac);
+ if (mctx == NULL) {
+ goto cleanup;
+ }
+ params[0] = OSSL_PARAM_construct_utf8_string(OSSL_MAC_PARAM_CIPHER,
+ cbc_name, 0);
+ params[1] = OSSL_PARAM_construct_end();
+
+ /* D = CMAC(K1, <zero>) */
+ if (!siv_cmac(mctx, key, k1_len, params, zero, sizeof(zero), d)) {
+ goto cleanup;
+ }
+
+ /* D = dbl(D) xor CMAC(K1, S_i) for each associated-data component */
+ if (ad != NULL && ad_len > 0) {
+ if (!siv_cmac(mctx, key, k1_len, params, ad, ad_len, t)) {
+ goto cleanup;
+ }
+ siv_dbl(d);
+ for (i = 0; i < SIV_TAG_LEN; i++) {
+ d[i] ^= t[i];
+ }
+ }
+ if (nonce != NULL && nonce_len > 0) {
+ if (!siv_cmac(mctx, key, k1_len, params, nonce, nonce_len, t)) {
+ goto cleanup;
+ }
+ siv_dbl(d);
+ for (i = 0; i < SIV_TAG_LEN; i++) {
+ d[i] ^= t[i];
+ }
+ }
+
+ /* Empty final component: T = dbl(D) xor pad(""), tag = CMAC(K1, T) */
+ siv_dbl(d);
+ d[0] ^= 0x80;
+ ret = siv_cmac(mctx, key, k1_len, params, d, SIV_TAG_LEN, tag);
+
+cleanup:
+ EVP_MAC_CTX_free(mctx);
+ EVP_MAC_free(mac);
+ return ret;
+}
+
int AES_SIV_Encrypt(AES_SIV_CTX *ctx, unsigned char *out, size_t *out_len,
unsigned char const *key, size_t key_len,
unsigned char const *nonce, size_t nonce_len,
@@ -112,6 +232,15 @@ int AES_SIV_Encrypt(AES_SIV_CTX *ctx, unsigned char *out, size_t *out_len,
return 0;
}
+ if (plaintext_len == 0) {
+ if (!siv_empty_tag(out, key, key_len, nonce, nonce_len, ad, ad_len)) {
+ OPENSSL_cleanse(out, SIV_TAG_LEN);
+ return 0;
+ }
+ *out_len = SIV_TAG_LEN;
+ return 1;
+ }
+
cipher_name = get_cipher_name(key_len);
if (cipher_name == NULL) {
return 0;
@@ -163,10 +292,8 @@ int AES_SIV_Encrypt(AES_SIV_CTX *ctx, unsigned char *out, size_t *out_len,
tag_out = out;
ct_out = out + SIV_TAG_LEN;
- /* Encrypt the plaintext (use empty buffer if NULL to satisfy OpenSSL) */
- unsigned char empty = 0;
- const unsigned char *pt = (plaintext != NULL) ? plaintext : ∅
- if (EVP_EncryptUpdate(ctx->cipher_ctx, ct_out, &len, pt, (int)plaintext_len) != 1) {
+ /* Encrypt the plaintext */
+ if (EVP_EncryptUpdate(ctx->cipher_ctx, ct_out, &len, plaintext, (int)plaintext_len) != 1) {
goto cleanup;
}
@@ -202,7 +329,7 @@ int AES_SIV_Decrypt(AES_SIV_CTX *ctx, unsigned char *out, size_t *out_len,
int len;
int ret = 0;
size_t plaintext_len;
- const unsigned char *tag_in;
+ unsigned char tag_in[SIV_TAG_LEN];
const unsigned char *ct_in;
if (ctx == NULL || ctx->cipher_ctx == NULL) {
@@ -221,6 +348,17 @@ int AES_SIV_Decrypt(AES_SIV_CTX *ctx, unsigned char *out, size_t *out_len,
return 0;
}
+ if (plaintext_len == 0) {
+ unsigned char expected[SIV_TAG_LEN];
+
+ if (!siv_empty_tag(expected, key, key_len, nonce, nonce_len, ad, ad_len) ||
+ CRYPTO_memcmp(expected, ciphertext, SIV_TAG_LEN) != 0) {
+ return 0;
+ }
+ *out_len = 0;
+ return 1;
+ }
+
cipher_name = get_cipher_name(key_len);
if (cipher_name == NULL) {
return 0;
@@ -243,12 +381,13 @@ int AES_SIV_Decrypt(AES_SIV_CTX *ctx, unsigned char *out, size_t *out_len,
/*
* SIV input format: tag (16 bytes) || ciphertext
*/
- tag_in = ciphertext;
+ /* Copy the tag: EVP_CIPHER_CTX_ctrl() takes a non-const pointer. */
+ memcpy(tag_in, ciphertext, SIV_TAG_LEN);
ct_in = ciphertext + SIV_TAG_LEN;
/* Set the expected tag for verification */
if (EVP_CIPHER_CTX_ctrl(ctx->cipher_ctx, EVP_CTRL_AEAD_SET_TAG,
- SIV_TAG_LEN, (void *)tag_in) != 1) {
+ SIV_TAG_LEN, tag_in) != 1) {
goto cleanup;
}
@@ -266,20 +405,15 @@ int AES_SIV_Decrypt(AES_SIV_CTX *ctx, unsigned char *out, size_t *out_len,
}
}
- /* Decrypt the ciphertext (use empty buffers if NULL to satisfy OpenSSL) */
- unsigned char empty = 0;
- unsigned char *out_ptr = (out != NULL) ? out : ∅
- const unsigned char *ct_ptr = (plaintext_len > 0) ? ct_in : ∅
- if (EVP_DecryptUpdate(ctx->cipher_ctx, out_ptr, &len, ct_ptr, (int)plaintext_len) != 1) {
+ /* Decrypt the ciphertext */
+ if (EVP_DecryptUpdate(ctx->cipher_ctx, out, &len, ct_in, (int)plaintext_len) != 1) {
goto cleanup;
}
/* Finalize and verify authentication tag */
- if (EVP_DecryptFinal_ex(ctx->cipher_ctx, out_ptr + len, &len) != 1) {
+ if (EVP_DecryptFinal_ex(ctx->cipher_ctx, out + len, &len) != 1) {
/* Authentication failed - clear output */
- if (out != NULL && plaintext_len > 0) {
- OPENSSL_cleanse(out, plaintext_len);
- }
+ OPENSSL_cleanse(out, plaintext_len);
goto cleanup;
}
=====================================
libntp/wscript
=====================================
@@ -11,7 +11,6 @@ from waflib.Logs import pprint
def build(ctx):
libntp_source = [
- "aes_siv_evp.c",
"authkeys.c",
"authreadkeys.c",
"clocktime.c",
@@ -31,6 +30,11 @@ def build(ctx):
"syssignal.c",
]
+ if not ctx.env.DISABLE_NTS:
+ libntp_source += [
+ "aes_siv_evp.c",
+ ]
+
libntp_source_sharable = [
"assert.c",
"clockwork.c",
View it on GitLab: https://gitlab.com/NTPsec/ntpsec/-/commit/7735674eade880b881a19e992cea5255d354136e
--
View it on GitLab: https://gitlab.com/NTPsec/ntpsec/-/commit/7735674eade880b881a19e992cea5255d354136e
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.ntpsec.org/pipermail/vc/attachments/20261003/ebec0a38/attachment-0001.htm>
More information about the vc
mailing list