diff options
| author | Dimitri Staessens <dimitri@ouroboros.rocks> | 2026-06-13 10:18:17 +0200 |
|---|---|---|
| committer | Sander Vrijders <sander@ouroboros.rocks> | 2026-06-29 08:32:58 +0200 |
| commit | 22e2380b09730a2f18deefd688585edb430d3299 (patch) | |
| tree | 1fc03db35d93833220482f9c5f70d4c9d2d618c1 /src/lib/tests/crypt_test.c | |
| parent | df14e6cc81c296d91e9124cd09f25a83defb522f (diff) | |
| download | ouroboros-22e2380b09730a2f18deefd688585edb430d3299.tar.gz ouroboros-22e2380b09730a2f18deefd688585edb430d3299.zip | |
lib: Harden symmetric-key rotation
Flow crypto signalled rotation with a single phase-parity bit, so a
loss burst that hid an even number of rotations went unnoticed and
wedged the flow for good.
Each packet now carries a small cleartext selector naming its key
directly, so a receiver that falls behind recovers on the next packet
instead of getting stuck.
The selector also serves as the AEAD nonce and is authenticated as
associated data (AAD). Key rotation moves into a new backend-agnostic
keyrot module that rotates sub-keys to bound AEAD usage while
preserving forward secrecy.
Signed-off-by: Dimitri Staessens <dimitri@ouroboros.rocks>
Signed-off-by: Sander Vrijders <sander@ouroboros.rocks>
Diffstat (limited to 'src/lib/tests/crypt_test.c')
| -rw-r--r-- | src/lib/tests/crypt_test.c | 430 |
1 files changed, 263 insertions, 167 deletions
diff --git a/src/lib/tests/crypt_test.c b/src/lib/tests/crypt_test.c index 028c4eb5..2d752238 100644 --- a/src/lib/tests/crypt_test.c +++ b/src/lib/tests/crypt_test.c @@ -30,6 +30,7 @@ #include <stdio.h> #define TEST_PACKET_SIZE 1500 +#define TEST_N_PACKETS 1000 extern const uint16_t crypt_supported_nids[]; extern const uint16_t md_supported_nids[]; @@ -39,9 +40,10 @@ static int test_crypt_create_destroy(void) struct crypt_ctx * ctx; uint8_t key[SYMMKEYSZ]; struct crypt_sk sk = { - .nid = NID_aes_256_gcm, - .key = key, - .rot_bit = KEY_ROTATION_BIT + .nid = NID_aes_256_gcm, + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT }; TEST_START(); @@ -67,18 +69,27 @@ static int test_crypt_create_destroy(void) static int test_crypt_encrypt_decrypt(int nid) { uint8_t pkt[TEST_PACKET_SIZE]; - struct crypt_ctx * ctx; + struct crypt_ctx * tx; + struct crypt_ctx * rx; uint8_t key[SYMMKEYSZ]; - struct crypt_sk sk = { - .nid = NID_aes_256_gcm, - .key = key, - .rot_bit = KEY_ROTATION_BIT + struct crypt_sk sk_tx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + struct crypt_sk sk_rx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_RESP }; buffer_t in; buffer_t out; buffer_t out2; const char * cipher; + sk_tx.nid = nid; + sk_rx.nid = nid; + cipher = crypt_nid_to_str(nid); TEST_START("(%s)", cipher); @@ -92,53 +103,63 @@ static int test_crypt_encrypt_decrypt(int nid) goto fail_init; } - ctx = crypt_create_ctx(&sk); - if (ctx == NULL) { - printf("Failed to initialize cryptography.\n"); + tx = crypt_create_ctx(&sk_tx); + if (tx == NULL) { + printf("Failed to initialize TX cryptography.\n"); goto fail_init; } + rx = crypt_create_ctx(&sk_rx); + if (rx == NULL) { + printf("Failed to initialize RX cryptography.\n"); + goto fail_tx; + } + in.len = sizeof(pkt); in.data = pkt; - if (crypt_encrypt(ctx, in, &out) < 0) { + if (crypt_encrypt(tx, in, &out) < 0) { printf("Encryption failed.\n"); goto fail_encrypt; } if (out.len < in.len) { printf("Encryption returned too little data.\n"); - goto fail_encrypt; + goto fail_chk; } - if (crypt_decrypt(ctx, out, &out2) < 0) { + if (crypt_decrypt(rx, out, &out2) < 0) { printf("Decryption failed.\n"); goto fail_decrypt; } if (out2.len != in.len) { printf("Decrypted data length does not match original.\n"); - goto fail_chk; + goto fail_chk2; } if (memcmp(in.data, out2.data, in.len) != 0) { printf("Decrypted data does not match original.\n"); - goto fail_chk; + goto fail_chk2; } - crypt_destroy_ctx(ctx); freebuf(out2); freebuf(out); + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); TEST_SUCCESS("(%s)", cipher); return TEST_RC_SUCCESS; - fail_chk: + fail_chk2: freebuf(out2); fail_decrypt: + fail_chk: freebuf(out); fail_encrypt: - crypt_destroy_ctx(ctx); + crypt_destroy_ctx(rx); + fail_tx: + crypt_destroy_ctx(tx); fail_init: TEST_FAIL("(%s)", cipher); return TEST_RC_FAIL; @@ -155,6 +176,214 @@ static int test_encrypt_decrypt_all(void) return ret; } +static int test_crypt_multi_packet(int nid) +{ + uint8_t pkt[TEST_PACKET_SIZE]; + struct crypt_ctx * tx; + struct crypt_ctx * rx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk_tx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + struct crypt_sk sk_rx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_RESP + }; + buffer_t in; + buffer_t enc; + buffer_t dec; + const char * cipher; + int i; + + sk_tx.nid = nid; + sk_rx.nid = nid; + + cipher = crypt_nid_to_str(nid); + TEST_START("(%s)", cipher); + + if (random_buffer(key, sizeof(key)) < 0) { + printf("Failed to generate random key.\n"); + goto fail_init; + } + + if (random_buffer(pkt, sizeof(pkt)) < 0) { + printf("Failed to generate random data.\n"); + goto fail_init; + } + + tx = crypt_create_ctx(&sk_tx); + if (tx == NULL) { + printf("Failed to create TX context.\n"); + goto fail_init; + } + + rx = crypt_create_ctx(&sk_rx); + if (rx == NULL) { + printf("Failed to create RX context.\n"); + goto fail_tx; + } + + in.len = sizeof(pkt); + in.data = pkt; + + for (i = 0; i < TEST_N_PACKETS; i++) { + if (crypt_encrypt(tx, in, &enc) < 0) { + printf("Encryption failed at packet %d.\n", i); + goto fail_rx; + } + + if (crypt_decrypt(rx, enc, &dec) < 0) { + printf("Decryption failed at packet %d.\n", i); + freebuf(enc); + goto fail_rx; + } + + if (dec.len != in.len || + memcmp(in.data, dec.data, in.len) != 0) { + printf("Data mismatch at packet %d.\n", i); + freebuf(dec); + freebuf(enc); + goto fail_rx; + } + + freebuf(dec); + freebuf(enc); + } + + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + fail_rx: + crypt_destroy_ctx(rx); + fail_tx: + crypt_destroy_ctx(tx); + fail_init: + TEST_FAIL("(%s)", cipher); + return TEST_RC_FAIL; +} + +static int test_multi_packet_all(void) +{ + int ret = 0; + int i; + + for (i = 0; crypt_supported_nids[i] != NID_undef; i++) + ret |= test_crypt_multi_packet(crypt_supported_nids[i]); + + return ret; +} + +static int test_crypt_aad_tamper(int nid) +{ + uint8_t pkt[TEST_PACKET_SIZE]; + struct crypt_ctx * tx; + struct crypt_ctx * rx; + uint8_t key[SYMMKEYSZ]; + struct crypt_sk sk_tx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT + }; + struct crypt_sk sk_rx = { + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_RESP + }; + buffer_t in; + buffer_t enc; + buffer_t dec; + const char * cipher; + + sk_tx.nid = nid; + sk_rx.nid = nid; + + cipher = crypt_nid_to_str(nid); + TEST_START("(%s)", cipher); + + if (random_buffer(key, sizeof(key)) < 0) { + printf("Failed to generate random key.\n"); + goto fail_init; + } + + if (random_buffer(pkt, sizeof(pkt)) < 0) { + printf("Failed to generate random data.\n"); + goto fail_init; + } + + tx = crypt_create_ctx(&sk_tx); + if (tx == NULL) { + printf("Failed to create TX context.\n"); + goto fail_init; + } + + rx = crypt_create_ctx(&sk_rx); + if (rx == NULL) { + printf("Failed to create RX context.\n"); + goto fail_tx; + } + + /* Only AEAD ciphers bind the selector as AAD. */ + if (crypt_get_tagsz(tx) == 0) { + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + } + + in.len = sizeof(pkt); + in.data = pkt; + + if (crypt_encrypt(tx, in, &enc) < 0) { + printf("Encryption failed.\n"); + goto fail_rx; + } + + /* Flip a seq byte: epoch/node stay valid so the AEAD tag rejects. */ + enc.data[5] ^= 0x01; + + if (crypt_decrypt(rx, enc, &dec) == 0) { + printf("Decryption accepted a tampered selector.\n"); + freebuf(dec); + freebuf(enc); + goto fail_rx; + } + + freebuf(enc); + + crypt_destroy_ctx(rx); + crypt_destroy_ctx(tx); + + TEST_SUCCESS("(%s)", cipher); + + return TEST_RC_SUCCESS; + fail_rx: + crypt_destroy_ctx(rx); + fail_tx: + crypt_destroy_ctx(tx); + fail_init: + TEST_FAIL("(%s)", cipher); + return TEST_RC_FAIL; +} + +static int test_aad_tamper_all(void) +{ + int ret = 0; + int i; + + for (i = 0; crypt_supported_nids[i] != NID_undef; i++) + ret |= test_crypt_aad_tamper(crypt_supported_nids[i]); + + return ret; +} + #ifdef HAVE_OPENSSL #include <openssl/evp.h> #include <openssl/obj_mac.h> @@ -256,109 +485,17 @@ static int test_md_nid_values(void) } #endif -static int test_key_rotation(void) +static int test_crypt_headsz(void) { - uint8_t pkt[TEST_PACKET_SIZE]; - struct crypt_ctx * tx_ctx; - struct crypt_ctx * rx_ctx; - uint8_t key[SYMMKEYSZ]; - struct crypt_sk sk = { - .nid = NID_aes_256_gcm, - .key = key, - .rot_bit = 7 - }; - buffer_t in; - buffer_t enc; - buffer_t dec; - uint32_t i; - uint32_t threshold; - - TEST_START(); - - if (random_buffer(key, sizeof(key)) < 0) { - printf("Failed to generate random key.\n"); - goto fail; - } - - if (random_buffer(pkt, sizeof(pkt)) < 0) { - printf("Failed to generate random data.\n"); - goto fail; - } - - tx_ctx = crypt_create_ctx(&sk); - if (tx_ctx == NULL) { - printf("Failed to create TX context.\n"); - goto fail; - } - - rx_ctx = crypt_create_ctx(&sk); - if (rx_ctx == NULL) { - printf("Failed to create RX context.\n"); - goto fail_tx; - } - - in.len = sizeof(pkt); - in.data = pkt; - - threshold = (1U << sk.rot_bit); - - /* Encrypt and decrypt across multiple rotations */ - for (i = 0; i < threshold * 3; i++) { - if (crypt_encrypt(tx_ctx, in, &enc) < 0) { - printf("Encryption failed at packet %u.\n", i); - goto fail_rx; - } - - if (crypt_decrypt(rx_ctx, enc, &dec) < 0) { - printf("Decryption failed at packet %u.\n", i); - freebuf(enc); - goto fail_rx; - } - - if (dec.len != in.len || - memcmp(in.data, dec.data, in.len) != 0) { - printf("Data mismatch at packet %u.\n", i); - freebuf(dec); - freebuf(enc); - goto fail_rx; - } - - freebuf(dec); - freebuf(enc); - } - - crypt_destroy_ctx(rx_ctx); - crypt_destroy_ctx(tx_ctx); - - TEST_SUCCESS(); - - return TEST_RC_SUCCESS; - fail_rx: - crypt_destroy_ctx(rx_ctx); - fail_tx: - crypt_destroy_ctx(tx_ctx); - fail: - TEST_FAIL(); - return TEST_RC_FAIL; -} - -static int test_key_phase_bit(void) -{ - uint8_t pkt[TEST_PACKET_SIZE]; struct crypt_ctx * ctx; uint8_t key[SYMMKEYSZ]; struct crypt_sk sk = { - .nid = NID_aes_256_gcm, - .key = key, - .rot_bit = 7 + .nid = NID_aes_256_gcm, + .key = key, + .epoch = 0, + .role = CRYPT_ROLE_INIT }; - buffer_t in; - buffer_t out; - uint32_t count; - uint32_t threshold; - uint8_t phase_before; - uint8_t phase_after; - int ivsz; + int headsz; TEST_START(); @@ -367,58 +504,15 @@ static int test_key_phase_bit(void) goto fail; } - if (random_buffer(pkt, sizeof(pkt)) < 0) { - printf("Failed to generate random data.\n"); - goto fail; - } - ctx = crypt_create_ctx(&sk); if (ctx == NULL) { printf("Failed to initialize cryptography.\n"); goto fail; } - ivsz = crypt_get_ivsz(ctx); - if (ivsz <= 0) { - printf("Invalid IV size.\n"); - goto fail_ctx; - } - - in.len = sizeof(pkt); - in.data = pkt; - - /* Encrypt packets up to just before rotation threshold */ - threshold = (1U << sk.rot_bit); - - /* Encrypt threshold - 1 packets (indices 0 to threshold-2) */ - for (count = 0; count < threshold - 1; count++) { - if (crypt_encrypt(ctx, in, &out) < 0) { - printf("Encryption failed at count %u.\n", count); - goto fail_ctx; - } - freebuf(out); - } - - /* Packet at index threshold-1: phase should still be initial */ - if (crypt_encrypt(ctx, in, &out) < 0) { - printf("Encryption failed before rotation.\n"); - goto fail_ctx; - } - phase_before = (out.data[0] & 0x80) ? 1 : 0; - freebuf(out); - - /* Packet at index threshold: phase should have toggled */ - if (crypt_encrypt(ctx, in, &out) < 0) { - printf("Encryption failed at rotation threshold.\n"); - goto fail_ctx; - } - phase_after = (out.data[0] & 0x80) ? 1 : 0; - freebuf(out); - - /* Phase bit should have toggled */ - if (phase_before == phase_after) { - printf("Phase bit did not toggle: before=%u, after=%u.\n", - phase_before, phase_after); + headsz = crypt_get_headsz(ctx); + if (headsz != 6) { + printf("Unexpected header size: %d (expected 6).\n", headsz); goto fail_ctx; } @@ -447,11 +541,13 @@ int crypt_test(int argc, #ifdef HAVE_OPENSSL ret |= test_cipher_nid_values(); ret |= test_md_nid_values(); - ret |= test_key_rotation(); - ret |= test_key_phase_bit(); + ret |= test_multi_packet_all(); + ret |= test_aad_tamper_all(); + ret |= test_crypt_headsz(); #else - (void) test_key_rotation; - (void) test_key_phase_bit; + (void) test_multi_packet_all; + (void) test_aad_tamper_all; + (void) test_crypt_headsz; return TEST_RC_SKIP; #endif |
