[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