[Git][NTPsec/ntpsec][2026-06-drop-libaes-siv] 3 commits: fix: build attic AES-SIV tools against EVP
Mark Atwood (@MarkAtwood)
gitlab at mg.gitlab.com
Sat Oct 3 03:52:31 UTC 2026
Mark Atwood pushed to branch 2026-06-drop-libaes-siv at NTPsec / ntpsec
Commits:
98d8f9cf by Mark Atwood at 2026-10-02T20:52:20-07:00
fix: build attic AES-SIV tools against EVP
attic/aes-siv-timing.c and attic/aead-timing.c still included the
removed libaes_siv header, breaking the feature-attic CI job. Include
aes_siv_evp.h, which provides the same API, and drop the stale
../libaes_siv include path and aes_siv use from attic/wscript. These
tools are already built only when NTS is enabled.
- - - - -
ceaa5dd5 by Mark Atwood at 2026-10-02T20:52:20-07:00
refactor: share S2V step, update AES-SIV docs
Fold the duplicated CMAC/dbl/xor blocks in siv_empty_tag() into
siv_s2v_step() so S2V component order lives in one place. Keep keying
CMAC on every step: on OpenSSL 3.0.0-3.0.2, EVP_MAC_init() with a NULL
key does not restart CMAC, so the cheaper restart gives wrong tags.
Document in aes_siv_evp.h that plaintext (encrypt) and out (decrypt)
may be NULL only for empty plaintext, and that *out_len is updated
only on success.
Update devel/hacking.adoc and devel/nts.adoc, which still described a
vendored libaes_siv, and state the OpenSSL 3.0 minimum.
- - - - -
ed629edd by Mark Atwood at 2026-10-02T20:52:20-07:00
test: add AES-SIV known-answer tests
The NTS tests only round-trip through aes_siv_evp.c, so a consistent
error in the hand-computed empty-plaintext tag would pass them. Add
known-answer tests for empty plaintext with 32/48/64-byte keys
(including tag-only decrypt with out=NULL and rejection of every
single-bit tag flip) and the RFC 5297 A.1 vector.
Expected values come from the removed libaes_siv and from
pycryptodome AES MODE_SIV, which agree byte for byte. The group runs
only when NTS is enabled.
Also drop the stale ../libaes_siv include and aes_siv use from
test_ntpd in tests/wscript.
- - - - -
10 changed files:
- attic/aead-timing.c
- attic/aes-siv-timing.c
- attic/wscript
- devel/hacking.adoc
- devel/nts.adoc
- include/aes_siv_evp.h
- libntp/aes_siv_evp.c
- tests/common/tests_main.c
- + tests/libntp/aes_siv_evp.c
- tests/wscript
Changes:
=====================================
attic/aead-timing.c
=====================================
@@ -37,7 +37,7 @@
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>
-#include "aes_siv.h"
+#include "aes_siv_evp.h"
#define UNUSED_ARG(arg) ((void)(arg))
#define INSIST(x) assert(x)
=====================================
attic/aes-siv-timing.c
=====================================
@@ -26,7 +26,7 @@
#include <openssl/err.h>
#include <openssl/rand.h>
-#include "aes_siv.h"
+#include "aes_siv_evp.h"
#include "ntp_fp.h"
#include "nts.h"
=====================================
attic/wscript
=====================================
@@ -19,9 +19,9 @@ def build(ctx):
ctx(
target=name,
features="c cprogram",
- includes=[ctx.bldnode.parent.abspath(), "../include", "../libaes_siv"],
+ includes=[ctx.bldnode.parent.abspath(), "../include"],
source=[name + ".c"],
- use="ntp M CRYPTO SSL RT PTHREAD aes_siv",
+ use="ntp M CRYPTO SSL RT PTHREAD",
install_path=None,
)
=====================================
devel/hacking.adoc
=====================================
@@ -73,8 +73,9 @@ https://medium.com/@porteneuve/mastering-git-subtrees-943d29a798ec
These don't even touch the biggest issue, which is stable reproducible
builds. For this reason, we use naive textual inclusion.
-So far this has only been an issue with libaes_siv. If you notice
-there's an inclusion not listed here, file an issue.
+This used to be an issue with libaes_siv; NTPsec now uses OpenSSL's
+native AES-SIV instead. If you notice there's an inclusion not listed
+here, file an issue.
== C Guidelines
@@ -209,14 +210,19 @@ Random isn't cryptographically secure. secure is.
We depend on the host distro to provide OpenSSL.
NTS-KE (key exchange) requires TLSv1.3 or newer.
-That means OpenSSL version 1.1.1 or newer.
+That means OpenSSL version 3.0 or newer.
Shared key authentication uses their crypto package.
We also use their RAND_bytes().
Note that OpenSSL 1.0.1 is no longer supported. 2020-Feb-17
Note that OpenSSL 1.1.0 is no longer supported. 2020-Mar-12
-We also use waf, libaes_siv, libjsmn, and unity but we have local
+NTS uses OpenSSL's native AES-SIV through the EVP interface, which
+needs OpenSSL 3.0 or newer; libntp/aes_siv_evp.c wraps it. OpenSSL
+before 3.5.0 cannot tag an empty plaintext, so for that case the
+wrapper computes the RFC 5297 S2V tag itself using OpenSSL's CMAC.
+
+We also use waf, libjsmn, and unity but we have local
copies rather than depending on the host distro to provide them.
=== Coding style and indentation
=====================================
devel/nts.adoc
=====================================
@@ -106,9 +106,10 @@ not a network connection.
- 1 to 8 cookies link:{draft-nts}#section-4.1.6[4.1.6]
- The selected AEAD algorithm link:{draft-nts}#section-4.1.5[4.1.5]
-For AEAD, we need libaes_siv.so, RFC 5297
-It's not in OpenSSL yet.
- https://github.com/dfoxfranke/libaes_siv
+For AEAD (AES-SIV, RFC 5297) we use OpenSSL's native EVP AES-SIV,
+which needs OpenSSL 3.0 or newer; see libntp/aes_siv_evp.c.
+OpenSSL before 3.5.0 cannot tag an empty plaintext, so for that case
+the wrapper computes the RFC 5297 S2V tag itself using OpenSSL's CMAC.
An NTP client SHOULD NOT initiate NTS-KE by default. Configuration
parameters from the NTPD config file will tell the NTPD client when and
=====================================
include/aes_siv_evp.h
=====================================
@@ -76,6 +76,9 @@ int AES_SIV_CTX_copy(AES_SIV_CTX *dst, AES_SIV_CTX const *src);
*
* Returns: 1 on success, 0 on failure
*
+ * plaintext may be NULL only when plaintext_len is 0.
+ * *out_len is updated only on success.
+ *
* Key length determines the SIV variant:
* 32 bytes -> AEAD_AES_SIV_CMAC_256 (AES-128)
* 48 bytes -> AEAD_AES_SIV_CMAC_384 (AES-192)
@@ -109,6 +112,9 @@ int AES_SIV_Encrypt(AES_SIV_CTX *ctx, unsigned char *out, size_t *out_len,
*
* Returns: 1 on success (authentication passed), 0 on failure
*
+ * out may be NULL only when the plaintext is empty (ciphertext_len is 16).
+ * *out_len is updated only on success.
+ *
* Key length determines the SIV variant:
* 32 bytes -> AEAD_AES_SIV_CMAC_256 (AES-128)
* 48 bytes -> AEAD_AES_SIV_CMAC_384 (AES-192)
=====================================
libntp/aes_siv_evp.c
=====================================
@@ -114,6 +114,11 @@ static void siv_dbl(unsigned char block[SIV_TAG_LEN]) {
(carry * 0x87));
}
+/*
+ * CMAC(K1, in). The key is passed on every call because on OpenSSL
+ * 3.0.0 to 3.0.2 EVP_MAC_init() with a NULL key does not restart CMAC,
+ * so a second MAC on the same context would continue the first.
+ */
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,
@@ -125,10 +130,37 @@ static int siv_cmac(EVP_MAC_CTX *mctx, const unsigned char *key, size_t key_len,
EVP_MAC_final(mctx, out, &out_len, SIV_TAG_LEN) == 1;
}
+/*
+ * D = dbl(D) xor CMAC(K1, S_i) for one non-final component.
+ * Empty components are skipped, as AES_SIV_Encrypt() does when it
+ * feeds them to OpenSSL.
+ */
+static int siv_s2v_step(EVP_MAC_CTX *mctx, const unsigned char *key,
+ size_t key_len, const OSSL_PARAM *params,
+ unsigned char d[SIV_TAG_LEN],
+ unsigned char const *in, size_t in_len) {
+ unsigned char t[SIV_TAG_LEN];
+ int i;
+
+ if (in == NULL || in_len == 0) {
+ return 1;
+ }
+ if (!siv_cmac(mctx, key, key_len, params, in, in_len, t)) {
+ return 0;
+ }
+ siv_dbl(d);
+ for (i = 0; i < SIV_TAG_LEN; i++) {
+ d[i] ^= t[i];
+ }
+ return 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.
+ *
+ * Note: this fetches CMAC and keys it (4 times) on every call. The cost
+ * is unmeasured; if it shows up on a busy NTS server, cache the fetched
+ * EVP_MAC in AES_SIV_CTX next to cipher_ctx.
*/
static int siv_empty_tag(unsigned char tag[SIV_TAG_LEN],
unsigned char const *key, size_t key_len,
@@ -142,10 +174,9 @@ static int siv_empty_tag(unsigned char tag[SIV_TAG_LEN],
EVP_MAC *mac;
EVP_MAC_CTX *mctx = NULL;
OSSL_PARAM params[2];
- unsigned char d[SIV_TAG_LEN], t[SIV_TAG_LEN];
+ unsigned char d[SIV_TAG_LEN];
size_t k1_len = key_len / 2;
int ret = 0;
- int i;
switch (key_len) {
case 32:
@@ -173,31 +204,13 @@ static int siv_empty_tag(unsigned char tag[SIV_TAG_LEN],
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)) {
+ /* D = CMAC(K1, <zero>), then fold in AD and nonce, in that order */
+ if (!siv_cmac(mctx, key, k1_len, params, zero, sizeof(zero), d) ||
+ !siv_s2v_step(mctx, key, k1_len, params, d, ad, ad_len) ||
+ !siv_s2v_step(mctx, key, k1_len, params, d, nonce, nonce_len)) {
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;
=====================================
tests/common/tests_main.c
=====================================
@@ -39,6 +39,9 @@ static void RunAllTests(void)
#endif
#ifdef TEST_LIBNTP
+#ifndef DISABLE_NTS
+ RUN_TEST_GROUP(aes_siv_evp);
+#endif
RUN_TEST_GROUP(authkeys);
RUN_TEST_GROUP(calendar);
RUN_TEST_GROUP(clocktime);
=====================================
tests/libntp/aes_siv_evp.c
=====================================
@@ -0,0 +1,156 @@
+/*
+ * Copyright the NTPsec project contributors
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Known-answer tests for the AES-SIV wrapper in libntp/aes_siv_evp.c.
+ *
+ * The expected values were not produced by the code under test. The
+ * empty-plaintext tags were computed with two independent
+ * implementations, which agree byte for byte:
+ * - the libaes_siv library NTPsec vendored before it was replaced by
+ * aes_siv_evp.c (libaes_siv/aes_siv.c at commit c7461cb7)
+ * - pycryptodome AES.new(key, AES.MODE_SIV, nonce=nonce) with
+ * update(ad), then encrypt_and_digest(b"")
+ * The non-empty vector is RFC 5297 Appendix A.1.
+ */
+
+#include "config.h"
+#include "aes_siv_evp.h"
+
+#include <string.h>
+
+#include "unity.h"
+#include "unity_fixture.h"
+
+#define TAG_LEN 16
+
+TEST_GROUP(aes_siv_evp);
+
+static AES_SIV_CTX *ctx;
+static unsigned char key[64]; /* key[i] = i; first key_len bytes used */
+static unsigned char ad[48]; /* ad[i] = 0x80 + i */
+static unsigned char nonce[16]; /* nonce[i] = 0xc0 + i */
+
+TEST_SETUP(aes_siv_evp) {
+ size_t i;
+
+ for (i = 0; i < sizeof(key); i++) {
+ key[i] = (unsigned char)i;
+ }
+ for (i = 0; i < sizeof(ad); i++) {
+ ad[i] = (unsigned char)(0x80 + i);
+ }
+ for (i = 0; i < sizeof(nonce); i++) {
+ nonce[i] = (unsigned char)(0xc0 + i);
+ }
+ ctx = AES_SIV_CTX_new();
+ TEST_ASSERT_NOT_NULL(ctx);
+}
+
+TEST_TEAR_DOWN(aes_siv_evp) {
+ AES_SIV_CTX_free(ctx);
+}
+
+/*
+ * Empty plaintext with AD and nonce, as NTS uses it: the output is the
+ * 16-byte S2V tag alone. Decrypt must accept it without touching out
+ * (callers pass NULL) and must reject it with any single bit flipped.
+ */
+static void check_empty(size_t key_len, const unsigned char expected[TAG_LEN]) {
+ unsigned char tag[TAG_LEN];
+ size_t out_len = sizeof(tag);
+ size_t bit;
+
+ TEST_ASSERT_EQUAL_INT(1, AES_SIV_Encrypt(ctx, tag, &out_len,
+ key, key_len, nonce, sizeof(nonce),
+ NULL, 0, ad, sizeof(ad)));
+ TEST_ASSERT_EQUAL_UINT(TAG_LEN, out_len);
+ TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, tag, TAG_LEN);
+
+ out_len = 1234;
+ TEST_ASSERT_EQUAL_INT(1, AES_SIV_Decrypt(ctx, NULL, &out_len,
+ key, key_len, nonce, sizeof(nonce),
+ expected, TAG_LEN, ad, sizeof(ad)));
+ TEST_ASSERT_EQUAL_UINT(0, out_len);
+
+ for (bit = 0; bit < 8 * TAG_LEN; bit++) {
+ memcpy(tag, expected, TAG_LEN);
+ tag[bit / 8] ^= (unsigned char)(1U << (bit % 8));
+ out_len = 0;
+ TEST_ASSERT_EQUAL_INT(0, AES_SIV_Decrypt(ctx, NULL, &out_len,
+ key, key_len, nonce, sizeof(nonce),
+ tag, TAG_LEN, ad, sizeof(ad)));
+ }
+}
+
+TEST(aes_siv_evp, EmptyPlaintext256) {
+ static const unsigned char expected[TAG_LEN] = {
+ 0xb6, 0xc8, 0x28, 0x92, 0x52, 0x30, 0xfc, 0xe6,
+ 0xdb, 0x8d, 0x74, 0xa4, 0x2c, 0x96, 0x01, 0xdc
+ };
+ check_empty(32, expected);
+}
+
+TEST(aes_siv_evp, EmptyPlaintext384) {
+ static const unsigned char expected[TAG_LEN] = {
+ 0xea, 0x2a, 0x49, 0x06, 0x43, 0x80, 0x99, 0xe1,
+ 0x98, 0xd4, 0xbc, 0x9e, 0xcc, 0x83, 0x4b, 0x8a
+ };
+ check_empty(48, expected);
+}
+
+TEST(aes_siv_evp, EmptyPlaintext512) {
+ static const unsigned char expected[TAG_LEN] = {
+ 0x2e, 0x41, 0xac, 0x72, 0x83, 0xef, 0x68, 0x48,
+ 0xf7, 0xb3, 0xac, 0x67, 0xbe, 0xc1, 0x67, 0x4f
+ };
+ check_empty(64, expected);
+}
+
+/* RFC 5297 Appendix A.1: deterministic AEAD, AD but no nonce. */
+TEST(aes_siv_evp, RFC5297_A1) {
+ static const unsigned char k[32] = {
+ 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8,
+ 0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf2, 0xf1, 0xf0,
+ 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
+ 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff
+ };
+ static const unsigned char a[24] = {
+ 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
+ 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
+ 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27
+ };
+ static const unsigned char p[14] = {
+ 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
+ 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee
+ };
+ static const unsigned char expected[30] = {
+ 0x85, 0x63, 0x2d, 0x07, 0xc6, 0xe8, 0xf3, 0x7f,
+ 0x95, 0x0a, 0xcd, 0x32, 0x0a, 0x2e, 0xcc, 0x93,
+ 0x40, 0xc0, 0x2b, 0x96, 0x90, 0xc4, 0xdc, 0x04,
+ 0xda, 0xef, 0x7f, 0x6a, 0xfe, 0x5c
+ };
+ unsigned char out[sizeof(expected)];
+ unsigned char pt[sizeof(p)];
+ size_t out_len = sizeof(out);
+
+ TEST_ASSERT_EQUAL_INT(1, AES_SIV_Encrypt(ctx, out, &out_len,
+ k, sizeof(k), NULL, 0,
+ p, sizeof(p), a, sizeof(a)));
+ TEST_ASSERT_EQUAL_UINT(sizeof(expected), out_len);
+ TEST_ASSERT_EQUAL_HEX8_ARRAY(expected, out, sizeof(expected));
+
+ out_len = sizeof(pt);
+ TEST_ASSERT_EQUAL_INT(1, AES_SIV_Decrypt(ctx, pt, &out_len,
+ k, sizeof(k), NULL, 0,
+ expected, sizeof(expected), a, sizeof(a)));
+ TEST_ASSERT_EQUAL_UINT(sizeof(p), out_len);
+ TEST_ASSERT_EQUAL_HEX8_ARRAY(p, pt, sizeof(p));
+}
+
+TEST_GROUP_RUNNER(aes_siv_evp) {
+ RUN_TEST_CASE(aes_siv_evp, EmptyPlaintext256);
+ RUN_TEST_CASE(aes_siv_evp, EmptyPlaintext384);
+ RUN_TEST_CASE(aes_siv_evp, EmptyPlaintext512);
+ RUN_TEST_CASE(aes_siv_evp, RFC5297_A1);
+}
=====================================
tests/wscript
=====================================
@@ -58,6 +58,9 @@ def build(ctx):
"libntp/ymd2yd.c"
] + common_source
+ if not ctx.env.DISABLE_NTS:
+ libntp_source += ["libntp/aes_siv_evp.c"]
+
ctx.ntp_test(
features="c cprogram test",
target="test_libntp",
@@ -109,11 +112,11 @@ def build(ctx):
ctx.ntp_test(
defines=unity_config + ["TEST_NTPD=1"],
features="c cprogram test",
- includes=[ctx.bldnode.parent.abspath(), "../include", "unity", "../ntpd", "common", "../libaes_siv"],
+ includes=[ctx.bldnode.parent.abspath(), "../include", "unity", "../ntpd", "common"],
install_path=None,
source=ntpd_source,
target="test_ntpd",
- use="ntpd_lib libntpd_obj unity ntp aes_siv "
+ use="ntpd_lib libntpd_obj unity ntp "
"M PTHREAD CRYPTO RT SOCKET NSL",
)
View it on GitLab: https://gitlab.com/NTPsec/ntpsec/-/compare/cb0aef44fa30fb086daaf54a1ae5643ba4f5e0dc...ed629edd48258eb416c45e21cf8c65369f698529
--
View it on GitLab: https://gitlab.com/NTPsec/ntpsec/-/compare/cb0aef44fa30fb086daaf54a1ae5643ba4f5e0dc...ed629edd48258eb416c45e21cf8c65369f698529
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/04f99fc8/attachment-0001.htm>
More information about the vc
mailing list