What changed, and why it matters
This commit hardens the Noise protocol code used in Trezor firmware by adding size limits and nonce limits to prevent two cryptographic problems: encrypting or decrypting messages that are too large for the protocol, and reusing the same encryption nonce after the counter wraps around. Reusing a nonce with AES-GCM can destroy the confidentiality of messages and allow attackers to forge fake messages. The patch also wipes sensitive context data more thoroughly when errors occur, reducing the chance that leftover key material could leak.
Treat this as a security hardening fix and include it in the next firmware release. Review all call sites that pass buffers to the Noise functions to ensure they respect the new NOISE_*_MAX_MESSAGE_SIZE and NOISE_*_MAX_PLAINTEXT_SIZE constants. Run the new test_noise_kk1_limits and test_noise_xxpsk3_limits tests in CI.
Security signals we found
AES-GCM nonce counter wraparound protection added
Maximum Noise message size enforced at runtime
Sensitive context zeroization expanded on failure paths
Output buffer zeroization guarded against NULL pointers
Static assertions added for fixed-size handshake messages
Evidence from the diff
The patch adds compile-time and runtime checks to the Noise KK1 and XXpsk3 implementations in crypto/noise_kk1.c/h and crypto/noise_xxpsk3.c/h. It enforces a 65535-byte Noise message size limit and a 48-bit nonce counter limit (2^48 messages), the latter to stay below NIST SP 800-38D’s AES-GCM block limit and to avoid nonce reuse. It also clears encryption contexts and output buffers on failure paths, including when the nonce counter reaches its limit. Tests are added to verify the new limits and failure behaviors.
Changed components
crypto/noise_kk1.ccrypto/noise_kk1.hcrypto/noise_xxpsk3.ccrypto/noise_xxpsk3.hcrypto/tests/test_check.cInspect captured patch +398 / −101
### crypto/noise_kk1.c
@@ -33,6 +33,13 @@ static uint8_t protocol_name[SHA256_DIGEST_LENGTH] = {
'5', '5', '1', '9', '_', 'A', 'E', 'S', 'G', 'C', 'M',
'_', 'S', 'H', 'A', '2', '5', '6', 0x00, 0x00, 0x00};
+// The KK1 handshake messages have a fixed size, so the Noise message size limit
+// can be checked at compile time instead of in the handshake functions
+_Static_assert(sizeof(noise_kk1_request_t) <= NOISE_KK1_MAX_MESSAGE_SIZE,
+ "handshake request must fit into a Noise message");
+_Static_assert(sizeof(noise_kk1_response_t) <= NOISE_KK1_MAX_MESSAGE_SIZE,
+ "handshake response must fit into a Noise message");
+
static bool encrypt(const uint8_t key[NOISE_KK1_KEY_SIZE],
const uint8_t nonce[NOISE_KK1_NONCE_SIZE],
const uint8_t *associated_data,
@@ -53,7 +60,9 @@ static bool encrypt(const uint8_t key[NOISE_KK1_KEY_SIZE],
ciphertext + plaintext_length, NOISE_KK1_TAG_SIZE,
&ctx) != RETURN_GOOD) {
memzero(&ctx, sizeof(ctx));
- memzero(ciphertext, plaintext_length);
+ if (ciphertext != NULL) {
+ memzero(ciphertext, plaintext_length + NOISE_KK1_TAG_SIZE);
+ }
return false;
}
memzero(&ctx, sizeof(ctx));
@@ -86,7 +95,9 @@ static bool decrypt(const uint8_t key[NOISE_KK1_KEY_SIZE],
ciphertext + plaintext_length, NOISE_KK1_TAG_SIZE,
&ctx) != RETURN_GOOD) {
memzero(&ctx, sizeof(ctx));
- memzero(plaintext, plaintext_length);
+ if (plaintext != NULL) {
+ memzero(plaintext, plaintext_length);
+ }
return false;
}
memzero(&ctx, sizeof(ctx));
@@ -147,10 +158,15 @@ static void split(uint8_t chaining_key[SHA256_DIGEST_LENGTH],
"output1 and output2 must be truncated to NOISE_KK1_KEY_SIZE");
}
+// The counter is restricted to (2^48)-1: 2^48 messages of at most 65535 bytes
+// produce at most 2^60 AES blocks under one key, well below the 2^64 blocks
+// per key that NIST SP 800-38D, Appendix B recommends as a limit.
+// https://tsapps.nist.gov/publication/get_pdf.cfm?pub_id=51288
static bool increase_nonce(uint8_t nonce[NOISE_KK1_NONCE_SIZE]) {
// The first 4 bytes of the nonce are zeros
- // The last 8 bytes of the nonce are a big-endian encoded counter
- for (int i = NOISE_KK1_NONCE_SIZE - 1; i >= 4; i--) {
+ // The last 8 bytes of the nonce are a big-endian encoded counter, of which
+ // only the low 6 bytes are ever used
+ for (int i = NOISE_KK1_NONCE_SIZE - 1; i >= 6; i--) {
nonce[i]++;
if (nonce[i] != 0) {
return true;
@@ -225,6 +241,7 @@ bool noise_kk1_handle_handshake_request(
if (!encrypt(kauth, zero_nonce, handshake_hash, sizeof(handshake_hash), NULL,
0, response->tag)) {
memzero(kauth, sizeof(kauth));
+ memzero(ctx, sizeof(*ctx));
return false;
}
memzero(kauth, sizeof(kauth));
@@ -286,6 +303,7 @@ bool noise_kk1_handle_handshake_response(
response->tag, NOISE_KK1_TAG_SIZE, NULL)) {
// Wrong tag
memzero(kauth, sizeof(kauth));
+ memzero(ctx, sizeof(*ctx));
return false;
}
memzero(kauth, sizeof(kauth));
@@ -309,6 +327,11 @@ bool noise_kk1_send_message(noise_kk1_context_t *ctx,
if (!ctx->initialized) {
return false;
}
+ if (associated_data_length > NOISE_KK1_MAX_PLAINTEXT_SIZE ||
+ plaintext_length >
+ NOISE_KK1_MAX_PLAINTEXT_SIZE - associated_data_length) {
+ return false;
+ }
if (!encrypt(ctx->encryption_key, ctx->encryption_nonce, associated_data,
associated_data_length, plaintext, plaintext_length,
ciphertext)) {
@@ -317,6 +340,7 @@ bool noise_kk1_send_message(noise_kk1_context_t *ctx,
if (!increase_nonce(ctx->encryption_nonce)) {
// Nonce overflow
memzero(ctx, sizeof(*ctx));
+ memzero(ciphertext, plaintext_length + NOISE_KK1_TAG_SIZE);
ctx->initialized = false;
return false;
}
@@ -332,6 +356,10 @@ bool noise_kk1_receive_message(noise_kk1_context_t *ctx,
if (!ctx->initialized) {
return false;
}
+ if (associated_data_length > NOISE_KK1_MAX_MESSAGE_SIZE ||
+ ciphertext_length > NOISE_KK1_MAX_MESSAGE_SIZE - associated_data_length) {
+ return false;
+ }
if (!decrypt(ctx->decryption_key, ctx->decryption_nonce, associated_data,
associated_data_length, ciphertext, ciphertext_length,
plaintext)) {
@@ -341,6 +369,9 @@ bool noise_kk1_receive_message(noise_kk1_context_t *ctx,
if (!increase_nonce(ctx->decryption_nonce)) {
// Nonce overflow
memzero(ctx, sizeof(*ctx));
+ if (plaintext != NULL) {
+ memzero(plaintext, ciphertext_length - NOISE_KK1_TAG_SIZE);
+ }
ctx->initialized = false;
return false;
}
### crypto/noise_kk1.h
@@ -34,6 +34,10 @@
#define NOISE_KK1_NONCE_SIZE 12
#define NOISE_KK1_TAG_SIZE 16
+#define NOISE_KK1_MAX_MESSAGE_SIZE 65535
+#define NOISE_KK1_MAX_PLAINTEXT_SIZE \
+ (NOISE_KK1_MAX_MESSAGE_SIZE - NOISE_KK1_TAG_SIZE)
+
typedef struct {
curve25519_key initiator_ephemeral_private_key; // This is used only by the
// initiator during handshake
@@ -85,6 +89,8 @@ bool noise_kk1_handle_handshake_response_multiple_keys(
// This is called by both the initiator and responder to send a message
// len(ciphertext) == plaintext_length + NOISE_KK1_TAG_SIZE
+// associated_data_length + plaintext_length must not exceed
+// NOISE_KK1_MAX_PLAINTEXT_SIZE
// The official Noise specification requires the associated_data to be empty
bool noise_kk1_send_message(noise_kk1_context_t* ctx,
const uint8_t* associated_data,
@@ -94,6 +100,8 @@ bool noise_kk1_send_message(noise_kk1_context_t* ctx,
// This is called by both the initiator and responder to receive a message
// len(plaintext) == ciphertext_length - NOISE_KK1_TAG_SIZE
+// associated_data_length + ciphertext_length must not exceed
+// NOISE_KK1_MAX_MESSAGE_SIZE
// The official Noise specification requires the associated_data to be empty
bool noise_kk1_receive_message(noise_kk1_context_t* ctx,
const uint8_t* associated_data,
### crypto/noise_xxpsk3.c
@@ -27,9 +27,13 @@
#include "rand.h"
#include "sha2.h"
-#define NONCE_LIMIT 0xFFFFFFFFFFFFFFFFULL
+// The counter is restricted to 48 bits: 2^48 messages of at most 65535 bytes
+// produce at most 2^60 AES blocks under one key, well below the 2^64 blocks
+// per key that NIST SP 800-38D, Appendix B recommends as a limit.
+// https://tsapps.nist.gov/publication/get_pdf.cfm?pub_id=51288
+#define NONCE_LIMIT 0x1000000000000ULL // 2^48
#define NONCE_ARRAY_SIZE_BYTES 12
-#define NOISE_TAG_SIZE_BYTES 16
+#define NOISE_TAG_SIZE_BYTES NOISE_XXPSK3_TAG_SIZE
/**
* @brief translate nonce into 12 byte big-endian array with
@@ -205,7 +209,11 @@ static void generate_keypair(uint8_t (*private_key)[NOISE_XXPSK3_DHLEN],
static bool encrypt_with_ad(noise_xxpsk3_cipher_state_t *cs, const uint8_t *ad,
size_t ad_len, const uint8_t *plaintext,
size_t plaintext_len, uint8_t *ciphertext) {
+ // A nonce at the limit is never used, so the counter below cannot wrap and no
+ // message is ever protected with a repeated nonce
if (!cs->has_key || cs->nonce >= NONCE_LIMIT) {
+ cs->has_key = false;
+ memzero(cs->key, NOISE_XXPSK3_HASHLEN);
return false;
} else {
// Encrypt with AEAD
@@ -227,7 +235,9 @@ static bool encrypt_with_ad(noise_xxpsk3_cipher_state_t *cs, const uint8_t *ad,
ciphertext + plaintext_len, NOISE_TAG_SIZE_BYTES,
&ctx) != RETURN_GOOD) {
memzero(&ctx, sizeof(ctx));
- memzero(ciphertext, plaintext_len + NOISE_TAG_SIZE_BYTES);
+ if (ciphertext != NULL) {
+ memzero(ciphertext, plaintext_len + NOISE_TAG_SIZE_BYTES);
+ }
memzero(nonce_bytes, sizeof(nonce_bytes));
return false;
}
@@ -257,8 +267,9 @@ static bool decrypt_with_ad(noise_xxpsk3_cipher_state_t *cs, const uint8_t *ad,
size_t ad_len, const uint8_t *ciphertext,
size_t ciphertext_len, uint8_t *plaintext) {
if (!cs->has_key || cs->nonce >= NONCE_LIMIT) {
+ cs->has_key = false;
+ memzero(cs->key, NOISE_XXPSK3_HASHLEN);
return false;
-
} else {
if (ciphertext_len < NOISE_TAG_SIZE_BYTES) {
// encrypted message is too short to contain the auth. tag
@@ -432,7 +443,8 @@ bool noise_xxpsk3_responder_handle_request1(
goto cleanup;
}
- if (request_len < NOISE_XXPSK3_DHLEN + NOISE_TAG_SIZE_BYTES) {
+ if (request_len < NOISE_XXPSK3_DHLEN + NOISE_TAG_SIZE_BYTES ||
+ request_len > NOISE_XXPSK3_MAX_MESSAGE_SIZE) {
goto cleanup;
}
@@ -488,6 +500,11 @@ bool noise_xxpsk3_responder_create_response1(
goto cleanup;
}
+ if (payload_size > NOISE_XXPSK3_MAX_MESSAGE_SIZE -
+ (2 * NOISE_XXPSK3_DHLEN + 2 * NOISE_TAG_SIZE_BYTES)) {
+ goto cleanup;
+ }
+
// Check if response buffer is large enough to hold the response
if (max_response_size <
(2 * NOISE_XXPSK3_DHLEN + 2 * NOISE_TAG_SIZE_BYTES + payload_size)) {
@@ -557,7 +574,8 @@ bool noise_xxpsk3_responder_handle_request2(
}
// Check if message is large enough to contain the encrypted remote static
// public key and at least empty encrypted payload (just NOISE_TAG)
- if (request_len < (NOISE_XXPSK3_DHLEN + 2 * NOISE_TAG_SIZE_BYTES)) {
+ if (request_len < (NOISE_XXPSK3_DHLEN + 2 * NOISE_TAG_SIZE_BYTES) ||
+ request_len > NOISE_XXPSK3_MAX_MESSAGE_SIZE) {
goto cleanup;
}
@@ -666,6 +684,11 @@ bool noise_xxpsk3_initiator_create_request1(
goto cleanup;
}
+ if (payload_size > NOISE_XXPSK3_MAX_MESSAGE_SIZE -
+ (NOISE_XXPSK3_DHLEN + NOISE_TAG_SIZE_BYTES)) {
+ goto cleanup;
+ }
+
if (max_request_size <
(NOISE_XXPSK3_DHLEN + payload_size + NOISE_TAG_SIZE_BYTES)) {
goto cleanup;
@@ -711,7 +734,8 @@ bool noise_xxpsk3_initiator_handle_response1(
goto cleanup;
}
- if (response_len < 2 * NOISE_XXPSK3_DHLEN + 2 * NOISE_TAG_SIZE_BYTES) {
+ if (response_len < 2 * NOISE_XXPSK3_DHLEN + 2 * NOISE_TAG_SIZE_BYTES ||
+ response_len > NOISE_XXPSK3_MAX_MESSAGE_SIZE) {
goto cleanup;
}
@@ -786,6 +810,11 @@ bool noise_xxpsk3_initiator_create_request2(
goto cleanup;
}
+ if (payload_size > NOISE_XXPSK3_MAX_MESSAGE_SIZE -
+ (NOISE_XXPSK3_DHLEN + 2 * NOISE_TAG_SIZE_BYTES)) {
+ goto cleanup;
+ }
+
if (max_request_size <
(NOISE_XXPSK3_DHLEN + 2 * NOISE_TAG_SIZE_BYTES + payload_size)) {
goto cleanup;
@@ -838,6 +867,10 @@ bool noise_xxpsk3_send_message(noise_xxpsk3_transport_state_t *ts,
return false;
}
+ if (payload_size > NOISE_XXPSK3_MAX_PLAINTEXT_SIZE) {
+ return false;
+ }
+
if (max_ciphertext_size < payload_size + NOISE_TAG_SIZE_BYTES) {
return false;
}
@@ -863,7 +896,11 @@ bool noise_xxpsk3_receive_message(noise_xxpsk3_transport_state_t *ts,
if (ciphertext_size < NOISE_TAG_SIZE_BYTES) {
return false;
}
- if (ciphertext_size > max_payload_size + NOISE_TAG_SIZE_BYTES) {
+ if (ciphertext_size > NOISE_XXPSK3_MAX_MESSAGE_SIZE) {
+ return false;
+ }
+ // The tag length check above rules out underflow
+ if (ciphertext_size - NOISE_TAG_SIZE_BYTES > max_payload_size) {
return false;
}
### crypto/noise_xxpsk3.h
@@ -25,6 +25,11 @@
#define NOISE_XXPSK3_HASHLEN 32
#define NOISE_XXPSK3_DHLEN 32
+#define NOISE_XXPSK3_TAG_SIZE 16
+
+#define NOISE_XXPSK3_MAX_MESSAGE_SIZE 65535
+#define NOISE_XXPSK3_MAX_PLAINTEXT_SIZE \
+ (NOISE_XXPSK3_MAX_MESSAGE_SIZE - NOISE_XXPSK3_TAG_SIZE)
// Uncomment to enable initiator/responder functionality in the noise protocol
// implementation.
@@ -345,7 +350,8 @@ bool noise_xxpsk3_responder_handle_request2(
*
* @param ts Pointer to the established transport state
* @param payload Plaintext to encrypt (may be empty)
- * @param payload_size Length of the plaintext in bytes
+ * @param payload_size Length of the plaintext in bytes; must not exceed
+ * NOISE_XXPSK3_MAX_PLAINTEXT_SIZE
* @param ciphertext Output buffer for the encrypted message
* @param max_ciphertext_size Size of the output buffer; must be at least
* payload_size + 16
@@ -364,6 +370,7 @@ bool noise_xxpsk3_send_message(noise_xxpsk3_transport_state_t *ts,
* @param ts Pointer to the established transport state
* @param ciphertext Encrypted message to decrypt
* @param ciphertext_size Length of the encrypted message; must be at least 16
+ * and must not exceed NOISE_XXPSK3_MAX_MESSAGE_SIZE
* @param payload Output buffer for the decrypted plaintext
* @param max_payload_size Size of the output buffer; must be at least
* ciphertext_size - 16
### crypto/tests/test_check.c
@@ -11866,10 +11866,14 @@ START_TEST(test_elligator2) {
}
END_TEST
-START_TEST(test_noise_kk1) {
- // Inject the seed to the random number generator to make the test
- // deterministic
- random_reseed(2748932008);
+// Runs the KK1 handshake and leaves both contexts in the transport phase. The
+// keys are derived from `seed`, so the exchanged messages are deterministic.
+static void test_noise_kk1_handshake(uint32_t seed,
+ noise_kk1_context_t *initiator_context,
+ noise_kk1_context_t *responder_context,
+ noise_kk1_request_t *request,
+ noise_kk1_response_t *response) {
+ random_reseed(seed);
curve25519_key initiator_private_key = {0};
curve25519_key responder_private_key = {0};
@@ -11880,6 +11884,24 @@ START_TEST(test_noise_kk1) {
curve25519_scalarmult_basepoint(initiator_public_key, initiator_private_key);
curve25519_scalarmult_basepoint(responder_public_key, responder_private_key);
+ // Initiator sends request
+ ck_assert_int_eq(
+ noise_kk1_create_handshake_request(initiator_context, request), true);
+
+ // Responder receives request and sends response
+ ck_assert_int_eq(noise_kk1_handle_handshake_request(
+ responder_context, initiator_public_key,
+ responder_private_key, request, response),
+ true);
+
+ // Initiator receives response
+ ck_assert_int_eq(noise_kk1_handle_handshake_response(
+ initiator_context, initiator_private_key,
+ responder_public_key, response),
+ true);
+}
+
+START_TEST(test_noise_kk1) {
noise_kk1_context_t initiator_context = {0};
noise_kk1_context_t responder_context = {0};
@@ -11921,24 +11943,11 @@ START_TEST(test_noise_kk1) {
bool ret = false;
- // Initiator sends request
- ret = noise_kk1_create_handshake_request(&initiator_context, &request);
- ck_assert_int_eq(ret, true);
+ test_noise_kk1_handshake(2748932008, &initiator_context, &responder_context,
+ &request, &response);
ck_assert_mem_eq(&request, fromhex(expected_request_hex), sizeof(request));
-
- // Responder receives request and sends response
- ret = noise_kk1_handle_handshake_request(
- &responder_context, initiator_public_key, responder_private_key, &request,
- &response);
- ck_assert_int_eq(ret, true);
ck_assert_mem_eq(&response, fromhex(expected_response_hex), sizeof(response));
- // Initiator receives response
- ret = noise_kk1_handle_handshake_response(&initiator_context,
- initiator_private_key,
- responder_public_key, &response);
- ck_assert_int_eq(ret, true);
-
// Initiator sends message1
ret = noise_kk1_send_message(&initiator_context, associated_data1,
sizeof(associated_data1), message1,
@@ -12001,10 +12010,122 @@ START_TEST(test_noise_kk1) {
}
END_TEST
-START_TEST(test_noise_xxpsk3) {
- // Inject the seed to the random number generator to make the test
- // deterministic
- random_reseed(2748932008);
+START_TEST(test_noise_kk1_limits) {
+ noise_kk1_context_t initiator_context = {0};
+ noise_kk1_context_t responder_context = {0};
+ noise_kk1_request_t request = {0};
+ noise_kk1_response_t response = {0};
+ bool ret = false;
+
+ test_noise_kk1_handshake(2748932008, &initiator_context, &responder_context,
+ &request, &response);
+
+ // --- Noise message size limit ---
+ // The buffers are large enough, it is the resulting Noise message that would
+ // exceed the 65535 byte limit
+ static uint8_t big_plaintext[NOISE_KK1_MAX_MESSAGE_SIZE + 1] = {0};
+ static uint8_t big_ciphertext[NOISE_KK1_MAX_MESSAGE_SIZE + 1] = {0};
+
+ ret =
+ noise_kk1_send_message(&initiator_context, NULL, 0, big_plaintext,
+ NOISE_KK1_MAX_PLAINTEXT_SIZE + 1, big_ciphertext);
+ ck_assert_int_eq(ret, false);
+ // The sentinel shows that the message was rejected for its size before it was
+ // decrypted, a failed tag verification would have wiped the output buffer
+ big_plaintext[0] = 0xA5;
+ ret =
+ noise_kk1_receive_message(&initiator_context, NULL, 0, big_ciphertext,
+ NOISE_KK1_MAX_MESSAGE_SIZE + 1, big_plaintext);
+ ck_assert_int_eq(ret, false);
+ ck_assert_uint_eq(big_plaintext[0], 0xA5);
+
+ // The largest allowed plaintext passes the size check and is encrypted
+ ret = noise_kk1_send_message(&initiator_context, NULL, 0, big_plaintext,
+ NOISE_KK1_MAX_PLAINTEXT_SIZE, big_ciphertext);
+ ck_assert_int_eq(ret, true);
+
+ // The associated data counts towards the same limit, so one byte of it makes
+ // the largest plaintext too long
+ ret = noise_kk1_send_message(&initiator_context, big_plaintext, 1,
+ big_plaintext, NOISE_KK1_MAX_PLAINTEXT_SIZE,
+ big_ciphertext);
+ ck_assert_int_eq(ret, false);
+ big_plaintext[0] = 0xA5;
+ ret = noise_kk1_receive_message(&initiator_context, big_plaintext, 1,
+ big_ciphertext, NOISE_KK1_MAX_MESSAGE_SIZE,
+ big_plaintext);
+ ck_assert_int_eq(ret, false);
+ ck_assert_uint_eq(big_plaintext[0], 0xA5);
+
+ // Shortening the plaintext by that one byte is accepted again
+ ret = noise_kk1_send_message(&initiator_context, big_plaintext, 1,
+ big_plaintext, NOISE_KK1_MAX_PLAINTEXT_SIZE - 1,
+ big_ciphertext);
+ ck_assert_int_eq(ret, true);
+
+ // An associated data length beyond the limit is rejected on its own
+ ret = noise_kk1_send_message(&initiator_context, big_plaintext,
+ NOISE_KK1_MAX_MESSAGE_SIZE + 1, big_plaintext, 0,
+ big_ciphertext);
+ ck_assert_int_eq(ret, false);
+
+ // --- Message limit ---
+ // The counter occupies the low 6 bytes of the nonce, so it is exhausted after
+ // 2^48 messages. The message that exhausts it is not released: both the
+ // ciphertext and the context are wiped rather than reused with a wrapped
+ // counter.
+ uint8_t message5[] = "message5";
+ uint8_t ciphertext5[sizeof(message5) + NOISE_KK1_TAG_SIZE] = {0};
+ uint8_t plaintext5[sizeof(message5)] = {0};
+ const uint8_t zeros5[sizeof(ciphertext5)] = {0};
+
+ memset(responder_context.encryption_nonce, 0, NOISE_KK1_NONCE_SIZE);
+ memset(responder_context.encryption_nonce + 6, 0xFF, 6);
+ ret = noise_kk1_send_message(&responder_context, NULL, 0, message5,
+ sizeof(message5), ciphertext5);
+ ck_assert_int_eq(ret, false);
+ ck_assert_int_eq(responder_context.initialized, false);
+ ck_assert_mem_eq(ciphertext5, zeros5, sizeof(ciphertext5));
+
+ // Sending again is refused because the context is gone
+ ret = noise_kk1_send_message(&responder_context, NULL, 0, message5,
+ sizeof(message5), ciphertext5);
+ ck_assert_int_eq(ret, false);
+
+ // The peer rejects the message that exhausts its counter even though it
+ // authenticates. No such message is ever released, so it has to be encrypted
+ // directly with the peer's key.
+ uint8_t exhausted_nonce[NOISE_KK1_NONCE_SIZE] = {0};
+ memset(exhausted_nonce + 6, 0xFF, 6);
+ gcm_ctx gcm_context = {0};
+ ck_assert_int_eq(gcm_init_and_key(initiator_context.decryption_key,
+ NOISE_KK1_KEY_SIZE, &gcm_context),
+ RETURN_GOOD);
+ memcpy(ciphertext5, message5, sizeof(message5));
+ ck_assert_int_eq(gcm_encrypt_message(exhausted_nonce, NOISE_KK1_NONCE_SIZE,
+ NULL, 0, ciphertext5, sizeof(message5),
+ ciphertext5 + sizeof(message5),
+ NOISE_KK1_TAG_SIZE, &gcm_context),
+ RETURN_GOOD);
+ memzero(&gcm_context, sizeof(gcm_context));
+
+ memset(initiator_context.decryption_nonce, 0, NOISE_KK1_NONCE_SIZE);
+ memset(initiator_context.decryption_nonce + 6, 0xFF, 6);
+ ret = noise_kk1_receive_message(&initiator_context, NULL, 0, ciphertext5,
+ sizeof(ciphertext5), plaintext5);
+ ck_assert_int_eq(ret, false);
+ ck_assert_int_eq(initiator_context.initialized, false);
+ ck_assert_mem_eq(plaintext5, zeros5, sizeof(plaintext5));
+}
+END_TEST
+
+// Runs the XXpsk3 handshake with empty payloads and leaves both sides in the
+// transport phase, checking the message sizes and the exchanged static keys.
+// The keys are derived from `seed`.
+static void test_noise_xxpsk3_handshake(uint32_t seed,
+ noise_xxpsk3_initiator_t *initiator,
+ noise_xxpsk3_responder_t *responder) {
+ random_reseed(seed);
uint8_t psk[32] = "this_is_a_32byte_preshared_key!!";
@@ -12018,71 +12139,64 @@ START_TEST(test_noise_xxpsk3) {
curve25519_scalarmult_basepoint(initiator_public_key, initiator_private_key);
curve25519_scalarmult_basepoint(responder_public_key, responder_private_key);
- noise_xxpsk3_initiator_t initiator = {0};
- noise_xxpsk3_responder_t responder = {0};
-
- bool ret = false;
-
- // Initialize initiator and responder
- ret = noise_xxpsk3_initiator_init(&initiator, psk, initiator_private_key,
- initiator_public_key);
- ck_assert_int_eq(ret, true);
-
- ret = noise_xxpsk3_responder_init(&responder, psk, responder_private_key,
- responder_public_key);
- ck_assert_int_eq(ret, true);
-
- // --- Handshake ---
-
- // Initiator creates request1
- uint8_t request1[256] = {0};
- size_t request1_size = 0;
- ret = noise_xxpsk3_initiator_create_request1(
- &initiator, NULL, 0, request1, sizeof(request1), &request1_size);
- ck_assert_int_eq(ret, true);
- ck_assert_int_eq(request1_size,
- 32 + 0 + 16); // NOISE_XXPSK3_DHLEN + payload + tag
+ ck_assert_int_eq(
+ noise_xxpsk3_initiator_init(initiator, psk, initiator_private_key,
+ initiator_public_key),
+ true);
+ ck_assert_int_eq(
+ noise_xxpsk3_responder_init(responder, psk, responder_private_key,
+ responder_public_key),
+ true);
- // Responder handles request1
- ret = noise_xxpsk3_responder_handle_request1(&responder, request1,
- request1_size, NULL, 0, NULL);
- ck_assert_int_eq(ret, true);
+ uint8_t request1[256] = {0}, response1[256] = {0}, request2[256] = {0};
+ size_t request1_size = 0, response1_size = 0, request2_size = 0;
+ uint8_t received_responder_public_key[32] = {0};
+ uint8_t received_initiator_public_key[32] = {0};
- // Responder creates response1
- uint8_t response1[256] = {0};
- size_t response1_size = 0;
- ret = noise_xxpsk3_responder_create_response1(
- &responder, NULL, 0, response1, sizeof(response1), &response1_size);
- ck_assert_int_eq(ret, true);
- // response1 = ephemeral_pub[32] + enc_static_pub[48] + enc_payload[0+16]
+ // request1 = ephemeral_public_key[32] + encrypted_payload[0 + 16]
+ ck_assert_int_eq(
+ noise_xxpsk3_initiator_create_request1(initiator, NULL, 0, request1,
+ sizeof(request1), &request1_size),
+ true);
+ ck_assert_int_eq(request1_size, 32 + 16);
+ ck_assert_int_eq(noise_xxpsk3_responder_handle_request1(
+ responder, request1, request1_size, NULL, 0, NULL),
+ true);
+
+ // response1 = ephemeral_public_key[32] + encrypted_static_public_key[48] +
+ // encrypted_payload[0 + 16]
+ ck_assert_int_eq(
+ noise_xxpsk3_responder_create_response1(
+ responder, NULL, 0, response1, sizeof(response1), &response1_size),
+ true);
ck_assert_int_eq(response1_size, 32 + 48 + 16);
-
- // Initiator handles response1
- uint8_t received_responder_public_key[32] = {0};
- ret = noise_xxpsk3_initiator_handle_response1(
- &initiator, response1, response1_size, received_responder_public_key,
- NULL, 0, NULL);
- ck_assert_int_eq(ret, true);
+ ck_assert_int_eq(noise_xxpsk3_initiator_handle_response1(
+ initiator, response1, response1_size,
+ received_responder_public_key, NULL, 0, NULL),
+ true);
ck_assert_mem_eq(received_responder_public_key, responder_public_key,
sizeof(responder_public_key));
- // Initiator creates request2
- uint8_t request2[256] = {0};
- size_t request2_size = 0;
- ret = noise_xxpsk3_initiator_create_request2(
- &initiator, NULL, 0, request2, sizeof(request2), &request2_size);
- ck_assert_int_eq(ret, true);
- // request2 = enc_static_pub[48] + enc_payload[0+16]
+ // request2 = encrypted_static_public_key[48] + encrypted_payload[0 + 16]
+ ck_assert_int_eq(
+ noise_xxpsk3_initiator_create_request2(initiator, NULL, 0, request2,
+ sizeof(request2), &request2_size),
+ true);
ck_assert_int_eq(request2_size, 48 + 16);
-
- // Responder handles request2 — handshake complete
- uint8_t received_initiator_public_key[32] = {0};
- ret = noise_xxpsk3_responder_handle_request2(
- &responder, request2, request2_size, received_initiator_public_key, NULL,
- 0, NULL);
- ck_assert_int_eq(ret, true);
+ ck_assert_int_eq(noise_xxpsk3_responder_handle_request2(
+ responder, request2, request2_size,
+ received_initiator_public_key, NULL, 0, NULL),
+ true);
ck_assert_mem_eq(received_initiator_public_key, initiator_public_key,
sizeof(initiator_public_key));
+}
+
+START_TEST(test_noise_xxpsk3) {
+ noise_xxpsk3_initiator_t initiator = {0};
+ noise_xxpsk3_responder_t responder = {0};
+ bool ret = false;
+
+ test_noise_xxpsk3_handshake(2748932008, &initiator, &responder);
// --- Transport phase: both directions ---
@@ -12164,20 +12278,23 @@ START_TEST(test_noise_xxpsk3) {
&pt_bad_size);
ck_assert_int_eq(ret, false);
+ // Both sides must have the same handshake hash.
+ ck_assert_mem_eq(initiator.transport_state.handshake_hash,
+ responder.transport_state.handshake_hash,
+ NOISE_XXPSK3_HASHLEN);
+
// --- Double-init should fail ---
- ret = noise_xxpsk3_initiator_init(&initiator, psk, initiator_private_key,
- initiator_public_key);
+ // The arguments are irrelevant, an already initialized structure is rejected
+ // before they are looked at
+ uint8_t unused_key[32] = {0};
+ ret = noise_xxpsk3_initiator_init(&initiator, unused_key, unused_key,
+ unused_key);
ck_assert_int_eq(ret, false);
- ret = noise_xxpsk3_responder_init(&responder, psk, responder_private_key,
- responder_public_key);
+ ret = noise_xxpsk3_responder_init(&responder, unused_key, unused_key,
+ unused_key);
ck_assert_int_eq(ret, false);
- // Both sides must have the same handshake hash
- ck_assert_mem_eq(initiator.transport_state.handshake_hash,
- responder.transport_state.handshake_hash,
- NOISE_XXPSK3_HASHLEN);
-
// Cleanup
noise_xxpsk3_initiator_deinit(&initiator);
noise_xxpsk3_responder_deinit(&responder);
@@ -12190,6 +12307,101 @@ START_TEST(test_noise_xxpsk3) {
}
END_TEST
+START_TEST(test_noise_xxpsk3_limits) {
+ // The buffers are large enough to hold an oversized Noise message, so only
+ // the message size limit itself can reject the calls below
+ static uint8_t payload_buf[NOISE_XXPSK3_MAX_MESSAGE_SIZE + 1] = {0};
+ static uint8_t message_buf[NOISE_XXPSK3_MAX_MESSAGE_SIZE + 1] = {0};
+ size_t message_size = 0;
+
+ noise_xxpsk3_initiator_t initiator = {0};
+ noise_xxpsk3_responder_t responder = {0};
+ bool ret = false;
+
+ // --- Handshake message size limit ---
+ // Any static key pair will do, the calls below are rejected on the length
+ // before any key is used
+ random_reseed(2748932008);
+ uint8_t psk[32] = "this_is_a_32byte_preshared_key!!";
+ uint8_t static_private_key[32] = {0};
+ uint8_t static_public_key[32] = {0};
+ random_buffer(static_private_key, sizeof(static_private_key));
+ curve25519_scalarmult_basepoint(static_public_key, static_private_key);
+
+ // request1 = ephemeral_public_key[32] + encrypted_payload[payload + 16]
+ ret = noise_xxpsk3_initiator_init(&initiator, psk, static_private_key,
+ static_public_key);
+ ck_assert_int_eq(ret, true);
+ ret = noise_xxpsk3_initiator_create_request1(
+ &initiator, payload_buf, NOISE_XXPSK3_MAX_MESSAGE_SIZE - (32 + 16) + 1,
+ message_buf, sizeof(message_buf), &message_size);
+ ck_assert_int_eq(ret, false);
+ // A rejected call deinitializes the initiator
+ ck_assert_int_eq(initiator.initialized, false);
+
+ ret = noise_xxpsk3_responder_init(&responder, psk, static_private_key,
+ static_public_key);
+ ck_assert_int_eq(ret, true);
+ // The sentinel shows that the message was rejected for its size before it was
+ // decrypted, a failed tag verification would have wiped the output buffer
+ payload_buf[0] = 0xA5;
+ ret = noise_xxpsk3_responder_handle_request1(
+ &responder, message_buf, NOISE_XXPSK3_MAX_MESSAGE_SIZE + 1, payload_buf,
+ sizeof(payload_buf), &message_size);
+ ck_assert_int_eq(ret, false);
+ ck_assert_int_eq(responder.initialized, false);
+ ck_assert_uint_eq(payload_buf[0], 0xA5);
+
+ // --- Transport message size limit ---
+ // The failed calls above wiped both structures, so a fresh handshake can run
+ test_noise_xxpsk3_handshake(2748932008, &initiator, &responder);
+
+ ret = noise_xxpsk3_send_message(&initiator.transport_state, payload_buf,
+ NOISE_XXPSK3_MAX_PLAINTEXT_SIZE + 1,
+ message_buf, sizeof(message_buf),
+ &message_size);
+ ck_assert_int_eq(ret, false);
+
+ payload_buf[0] = 0xA5;
+ ret = noise_xxpsk3_receive_message(&responder.transport_state, message_buf,
+ NOISE_XXPSK3_MAX_MESSAGE_SIZE + 1,
+ payload_buf, sizeof(payload_buf),
+ &message_size);
+ ck_assert_int_eq(ret, false);
+ ck_assert_uint_eq(payload_buf[0], 0xA5);
+
+ // The largest allowed plaintext passes the size check and is encrypted into a
+ // message of exactly the maximum size
+ ret = noise_xxpsk3_send_message(&initiator.transport_state, payload_buf,
+ NOISE_XXPSK3_MAX_PLAINTEXT_SIZE, message_buf,
+ sizeof(message_buf), &message_size);
+ ck_assert_int_eq(ret, true);
+ ck_assert_int_eq(message_size, NOISE_XXPSK3_MAX_MESSAGE_SIZE);
+
+ // --- Message limit ---
+ uint8_t msg[] = "hello";
+ uint8_t ciphertext[sizeof(msg) + 16] = {0};
+ size_t ciphertext_size = 0;
+
+ // The last message below the limit, with the nonce at 2^48 - 1, is still
+ // encrypted, which advances the nonce to the limit
+ responder.transport_state.send_cipher_state.nonce = (1ULL << 48) - 1;
+ ret = noise_xxpsk3_send_message(&responder.transport_state, msg, sizeof(msg),
+ ciphertext, sizeof(ciphertext),
+ &ciphertext_size);
+ ck_assert_int_eq(ret, true);
+
+ // With the nonce at 2^48 no further message is encrypted
+ ret = noise_xxpsk3_send_message(&responder.transport_state, msg, sizeof(msg),
+ ciphertext, sizeof(ciphertext),
+ &ciphertext_size);
+ ck_assert_int_eq(ret, false);
+
+ noise_xxpsk3_initiator_deinit(&initiator);
+ noise_xxpsk3_responder_deinit(&responder);
+}
+END_TEST
+
START_TEST(test_noise_xxpsk3_vectors) {
static const struct {
uint32_t seed;
@@ -12789,7 +13001,9 @@ Suite *test_suite(void) {
tc = tcase_create("noise");
tcase_add_test(tc, test_noise_kk1);
+ tcase_add_test(tc, test_noise_kk1_limits);
tcase_add_test(tc, test_noise_xxpsk3);
+ tcase_add_test(tc, test_noise_xxpsk3_limits);
tcase_add_test(tc, test_noise_xxpsk3_vectors);
suite_add_tcase(s, tc);
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.