[Git][NTPsec/ntpsec][master] nts: decrypt client AEEF fields into a real buffer

Gary E. Miller (@garyedmundsmiller) gitlab at mg.gitlab.com
Mon Oct 19 21:37:23 UTC 2026



Gary E. Miller pushed to branch master at NTPsec / ntpsec


Commits:
b826018d by Matt Selsky at 2026-10-19T14:30:15-07:00
nts: decrypt client AEEF fields into a real buffer

extens_server_recv() required the NTS Authenticator and Encrypted
Extension Fields field to hold exactly a 16-octet nonce and a 16-octet
ciphertext, so it rejected requests that RFC 8915 section 5.6 allows:
longer nonces, shorter nonces with Additional Padding, and encrypted
extension fields. It also passed AES_SIV_Decrypt() an output length of
6 for a buffer that could hold nothing.

Accept any nonce and ciphertext that fit in the field, and discard
packets whose nonce plus Additional Padding is shorter than 16 octets
(N_REQ for AES-SIV), as section 5.6 requires. Decrypt into a buffer
whose size is passed as the output length. The decrypted extension
fields are not used yet. Rename cmac to ciphertext, which is what the
field holds.

extens_client_send() and extens_server_recv() also passed NULL for the
empty plaintext and for the output buffer. libaes_siv allows NULL only
for the nonce, and with a NULL buffer do_s2v_p() calls memcpy() with a
NULL source, which is undefined behavior that UBSan reports from
test_ntpd. The client now passes "", and the server passes the new
buffer. libaes_siv's maintainer confirmed that plaintext must never be
NULL, even with zero length:
https://github.com/dfoxfranke/libaes_siv/issues/18

Add tests for a longer nonce with an encrypted extension field, and for
a short nonce with and without enough Additional Padding.

- - - - -


2 changed files:

- ntpd/nts_extens.c
- tests/ntpd/nts_extens.c


Changes:

=====================================
ntpd/nts_extens.c
=====================================
@@ -102,7 +102,7 @@ int extens_client_send(struct peer *peer, struct pkt *xpkt) {
                              buf.next, &left,
                              peer->nts_state.c2s, peer->nts_state.keylen,
                              nonce, NONCE_LENGTH,
-                             NULL, 0,           // no plain/cipher text
+                             (const uint8_t *)"", 0,  // no plaintext
                              packet, adlength);
         if (!ok) {
                 msyslog(LOG_ERR,
@@ -126,7 +126,7 @@ int extens_client_send(struct peer *peer, struct pkt *xpkt) {
 bool extens_server_recv(struct ntspacket_t *ntspacket, uint8_t *pkt, int lng) {
         struct BufCtl_t buf;
         uint16_t aead;
-        unsigned noncelen, cmaclen;
+        unsigned noncelen, ctlen, noncepad, ctpad, fieldlen;
         bool sawcookie, sawAEEF;
         int cookielen;                  // cookie and placeholder(s)
 
@@ -144,7 +144,8 @@ bool extens_server_recv(struct ntspacket_t *ntspacket, uint8_t *pkt, int lng) {
                 uint16_t type;
                 int length, adlength;
                 size_t outlen;
-                uint8_t *nonce, *cmac;
+                uint8_t *nonce, *ciphertext;
+                uint8_t plaintext[MAX_EXT_LEN];
                 bool ok;
 
                 type = ex_next_record(&buf, &length); // length excludes header
@@ -203,35 +204,40 @@ bool extens_server_recv(struct ntspacket_t *ntspacket, uint8_t *pkt, int lng) {
                         if (!sawcookie) {
                                 return false; // no cookie yet, no c2s
                         }
-                        if (length != NTP_EX_HDR_LNG+NONCE_LENGTH+CMAC_LENGTH) {
+                        if (length < NTP_EX_U16_LNG*2+NONCE_LENGTH+CMAC_LENGTH) {
                                 return false;
                         }
                         /* Additional data is up to this exten.
                          * backup over header */
                         adlength = buf.next-NTP_EX_HDR_LNG-pkt;
                         noncelen = next_uint16(&buf);
-                        cmaclen = next_uint16(&buf);
-                        if (NONCE_LENGTH != noncelen) {
+                        ctlen = next_uint16(&buf);
+                        // nonce, ciphertext, and additional padding
+                        fieldlen = (unsigned)length - NTP_EX_U16_LNG*2;
+                        noncepad = (noncelen + 3) & ~3U;
+                        ctpad = (ctlen + 3) & ~3U;
+                        if (noncepad + ctpad > fieldlen) {
                                 return false;
                         }
-                        if (CMAC_LENGTH != cmaclen) {
+                        // RFC 8915 5.6: nonce plus additional padding
+                        // must be at least N_REQ, 16 for AES-SIV
+                        if (fieldlen - ctpad < NONCE_LENGTH) {
                                 return false;
                         }
                         nonce = buf.next;
-                        cmac = nonce+NONCE_LENGTH;
-                        outlen = 6;
+                        ciphertext = nonce+noncepad;
+                        // Encrypted extension fields are decrypted
+                        // but not used.
+                        outlen = sizeof(plaintext);
                         ok = AES_SIV_Decrypt(wire_ctx,
-                                             NULL, &outlen,
+                                             plaintext, &outlen,
                                              ntspacket->c2s, ntspacket->keylen,
                                              nonce, noncelen,
-                                             cmac, CMAC_LENGTH,
+                                             ciphertext, ctlen,
                                              pkt, adlength);
                         if (!ok) {
                                 return false;
                         }
-                        if (0 != outlen) {
-                                return false;
-                        }
                         // we already used 2 length slots way above*/
                         length -= (NTP_EX_U16_LNG+NTP_EX_U16_LNG);
                         buf.next += length;


=====================================
tests/ntpd/nts_extens.c
=====================================
@@ -120,7 +120,7 @@ TEST(nts_extens, extens_server_recv) {
 	append_header(&buf, NTS_AEEF, NTP_EX_HDR_LNG+NONCE_LENGTH+CMAC_LENGTH);
 	ok = extens_server_recv(&ntspkt, pkt, sizeof(pkt));
 	TEST_ASSERT_EQUAL(false, ok);
-	/* Bad AEEF length */
+	/* Ciphertext shorter than the SIV tag (zero lengths) */
 	memset(pkt+LEN_PKT_NOMAC, 0, sizeof(pkt)-LEN_PKT_NOMAC);
 	buf.next = pkt+LEN_PKT_NOMAC;
 	buf.left = sizeof(pkt) - LEN_PKT_NOMAC;
@@ -128,9 +128,9 @@ TEST(nts_extens, extens_server_recv) {
 	append_header(&buf, NTS_AEEF, NTP_EX_HDR_LNG+NONCE_LENGTH+CMAC_LENGTH+8);
 	ok = extens_server_recv(&ntspkt, pkt, sizeof(pkt));
 	TEST_ASSERT_EQUAL(false, ok);
-	/* Bad noncelen */
+	/* Nonce runs past the end of the field */
 	uint8_t aeef_data1[NTP_EX_HDR_LNG+NONCE_LENGTH+CMAC_LENGTH] = {
-		0, 3, /* nonce length, bad, not mod 4 */
+		0, 32, /* nonce length, bad, longer than the field */
 		0, 16, /* cmac length */
 		1, 2, 3, /* nonce */
 		1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; /* cmac */
@@ -141,10 +141,10 @@ TEST(nts_extens, extens_server_recv) {
 	ex_append_record_bytes(&buf, NTS_AEEF, aeef_data1, sizeof(aeef_data1));
 	ok = extens_server_recv(&ntspkt, pkt, sizeof(pkt));
 	TEST_ASSERT_EQUAL(false, ok);
-	/* Bad CMAC length */
+	/* Ciphertext shorter than the SIV tag */
 	uint8_t aeef_data2[NTP_EX_HDR_LNG+NONCE_LENGTH+CMAC_LENGTH] = {
 		0, 16, /* nonce length */
-		0, 8, /* cmac length, bad != 16 */
+		0, 8, /* ciphertext length, bad < 16 */
 		1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, /* nonce */
 		1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; /* cmac */
 	memset(pkt+LEN_PKT_NOMAC, 0, sizeof(pkt)-LEN_PKT_NOMAC);
@@ -225,7 +225,86 @@ TEST(nts_extens, extens_server_recv) {
 	/* TEST_ASSERT_EQUAL(true, ok); //disable */
 }
 
+/* Build a request with a cookie and an NTS Authenticator and Encrypted
+ * Extension Fields field, and return its length */
+static int build_request(uint8_t *pkt, size_t pktlen,
+			 uint8_t *cookie, int cookielen, uint8_t *c2s,
+			 unsigned noncelen, unsigned addpad,
+			 const uint8_t *plain, size_t plainlen) {
+	BufCtl buf;
+	AES_SIV_CTX *ctx;
+	unsigned noncepad = (noncelen + 3) & ~3U;
+	unsigned ctlen = plainlen + CMAC_LENGTH;
+	unsigned ctpad = (ctlen + 3) & ~3U;
+	size_t outlen = ctpad;
+	uint8_t *nonce;
+	int adlength;
+
+	memset(pkt, 0, pktlen);
+	memcpy(pkt, base_pkt, sizeof(base_pkt));
+	buf.next = pkt+LEN_PKT_NOMAC;
+	buf.left = pktlen - LEN_PKT_NOMAC;
+	ex_append_record_bytes(&buf, NTS_Cookie, cookie, cookielen);
+	adlength = buf.next - pkt;
+	ex_append_header(&buf, NTS_AEEF,
+			 NTP_EX_U16_LNG*2 + noncepad + ctpad + addpad);
+	append_uint16(&buf, noncelen);
+	append_uint16(&buf, ctlen);
+	nonce = buf.next;
+	memset(nonce, 0xA5, noncelen);
+	ctx = AES_SIV_CTX_new();
+	TEST_ASSERT_TRUE(AES_SIV_Encrypt(ctx, nonce + noncepad, &outlen,
+					 c2s, 32, nonce, noncelen,
+					 plain, plainlen, pkt, adlength));
+	AES_SIV_CTX_free(ctx);
+	buf.next += noncepad + ctpad + addpad;
+	return buf.next - pkt;
+}
+
+TEST(nts_extens, extens_server_recv_rfc8915) {
+	struct ntspacket_t ntspkt;
+	uint8_t pkt[2048];
+	uint8_t cookie[NTS_MAX_COOKIELEN];
+	/* AEAD_AES_SIV_CMAC_256 keys are 32 octets */
+	uint8_t c2s[32] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
+	uint8_t s2c[32] = {16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
+	/* An unknown extension field, encrypted */
+	const uint8_t ext[] = {0x00, 0xFF, 0, 8, 1, 2, 3, 4};
+	int cookielen, lng;
+
+	nts_cookie_init();
+	nts_nKeys = 0;
+	nts_make_cookie_key();
+	cookielen = nts_make_cookie(cookie, AEAD_AES_SIV_CMAC_256, c2s, s2c,
+				    sizeof(c2s));
+
+	/* 16-octet nonce, no plaintext */
+	memset(&ntspkt, 0, sizeof(ntspkt));
+	lng = build_request(pkt, sizeof(pkt), cookie, cookielen, c2s,
+			    16, 0, (const uint8_t *)"", 0);
+	TEST_ASSERT_TRUE(extens_server_recv(&ntspkt, pkt, lng));
+
+	/* Longer nonce and an encrypted extension field */
+	memset(&ntspkt, 0, sizeof(ntspkt));
+	lng = build_request(pkt, sizeof(pkt), cookie, cookielen, c2s,
+			    24, 0, ext, sizeof(ext));
+	TEST_ASSERT_TRUE(extens_server_recv(&ntspkt, pkt, lng));
+
+	/* Short nonce with enough additional padding */
+	memset(&ntspkt, 0, sizeof(ntspkt));
+	lng = build_request(pkt, sizeof(pkt), cookie, cookielen, c2s,
+			    8, 8, (const uint8_t *)"", 0);
+	TEST_ASSERT_TRUE(extens_server_recv(&ntspkt, pkt, lng));
+
+	/* Short nonce without enough additional padding */
+	memset(&ntspkt, 0, sizeof(ntspkt));
+	lng = build_request(pkt, sizeof(pkt), cookie, cookielen, c2s,
+			    8, 4, ext, sizeof(ext));
+	TEST_ASSERT_FALSE(extens_server_recv(&ntspkt, pkt, lng));
+}
+
 TEST_GROUP_RUNNER(nts_extens) {
 	RUN_TEST_CASE(nts_extens, extens_client_send);
 	RUN_TEST_CASE(nts_extens, extens_server_recv);
+	RUN_TEST_CASE(nts_extens, extens_server_recv_rfc8915);
 }



View it on GitLab: https://gitlab.com/NTPsec/ntpsec/-/commit/b826018ddfff3f9c09b09872278a7ce850568674

-- 
View it on GitLab: https://gitlab.com/NTPsec/ntpsec/-/commit/b826018ddfff3f9c09b09872278a7ce850568674
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/20261019/51210a2f/attachment-0001.htm>


More information about the vc mailing list