diff options
| author | Dimitri Staessens <dimitri@ouroboros.rocks> | 2026-02-14 14:33:50 +0100 |
|---|---|---|
| committer | Sander Vrijders <sander@ouroboros.rocks> | 2026-02-18 07:52:56 +0100 |
| commit | d85326a119c34789055c388fcd18bb0161fbfd21 (patch) | |
| tree | 629b027f16bfcc6303609b915a4f0f86acc09fd8 /src/irmd/oap/tests/oap_test.c | |
| parent | 86dba5441c686d037c493e5b498e27249aa6bd9d (diff) | |
| download | ouroboros-d85326a119c34789055c388fcd18bb0161fbfd21.tar.gz ouroboros-d85326a119c34789055c388fcd18bb0161fbfd21.zip | |
irmd: Add strength-based crypto negotiation
Each side's configured cipher, KDF, and KEX algorithm now
represents a minimum security floor ("at least this strong").
Cipher and KDF use strongest-wins: the server compares ranks
and selects the stronger of client vs server config. The
negotiated values are sent in the response header. The client
verifies the server's response meets its own minimum, which
prevents downgrade attacks on the wire.
KEX uses a minimum-floor check: the server extracts the
client's algorithm from its public key and rejects if it
ranks below the server's configured algorithm. A server
configured with ML-KEM will reject all classical algorithms.
Special case: for client-encap KEM, the client has already
derived its key using its KDF, so the server must use the
same KDF and can only reject if it is too weak.
The supported_nids arrays are ordered weakest to strongest
and serve as the single source of truth for ranking.
Cipher ranking (weakest to strongest):
aes-128-ctr, aes-192-ctr, aes-256-ctr,
aes-128-gcm, aes-192-gcm, aes-256-gcm,
chacha20-poly1305
KDF ranking (weakest to strongest):
blake2s256, sha256, sha3-256, sha384,
sha3-384, blake2b512, sha512, sha3-512
KEX ranking (weakest to strongest):
ffdhe2048, prime256v1, X25519, ffdhe3072,
secp384r1, ffdhe4096, X448, secp521r1,
ML-KEM-512, ML-KEM-768, ML-KEM-1024,
X25519MLKEM768, X448MLKEM1024
Negotiation outcomes:
strong srv cipher + weak cli cipher -> use strongest
weak srv cipher + strong cli cipher -> use strongest
srv encryption + cli none -> server rejects
srv none + cli encryption -> use client's
strong srv KEX + weak cli KEX -> server rejects
weak srv KEX + strong cli KEX -> succeeds
wire tamper to weaker cipher -> client rejects
Signed-off-by: Dimitri Staessens <dimitri@ouroboros.rocks>
Signed-off-by: Sander Vrijders <sander@ouroboros.rocks>
Diffstat (limited to 'src/irmd/oap/tests/oap_test.c')
| -rw-r--r-- | src/irmd/oap/tests/oap_test.c | 262 |
1 files changed, 246 insertions, 16 deletions
diff --git a/src/irmd/oap/tests/oap_test.c b/src/irmd/oap/tests/oap_test.c index 70f0a248..fc78ed9a 100644 --- a/src/irmd/oap/tests/oap_test.c +++ b/src/irmd/oap/tests/oap_test.c @@ -536,7 +536,7 @@ static int test_oap_roundtrip_all(void) return ret; } -/* Cipher negotiation - client should accept server's chosen cipher */ +/* Cipher negotiation - strongest cipher and KDF are selected */ static int test_oap_cipher_mismatch(void) { struct oap_test_ctx ctx; @@ -545,17 +545,17 @@ static int test_oap_cipher_mismatch(void) memset(&test_cfg, 0, sizeof(test_cfg)); - /* Server: ChaCha20-Poly1305, SHA3-256, SHA-384 */ + /* Server: AES-128-GCM, SHA-256 */ test_cfg.srv.kex = NID_X25519; - test_cfg.srv.cipher = NID_chacha20_poly1305; - test_cfg.srv.kdf = NID_sha3_256; - test_cfg.srv.md = NID_sha384; + test_cfg.srv.cipher = NID_aes_128_gcm; + test_cfg.srv.kdf = NID_sha256; + test_cfg.srv.md = NID_sha256; test_cfg.srv.auth = AUTH; - /* Client: AES-256-GCM, SHA-256, SHA-256 */ + /* Client: AES-256-GCM, SHA-512 */ test_cfg.cli.kex = NID_X25519; test_cfg.cli.cipher = NID_aes_256_gcm; - test_cfg.cli.kdf = NID_sha256; + test_cfg.cli.kdf = NID_sha512; test_cfg.cli.md = NID_sha256; test_cfg.cli.auth = NO_AUTH; @@ -577,17 +577,17 @@ static int test_oap_cipher_mismatch(void) goto fail_cleanup; } - /* Verify: both should have the server's chosen cipher and KDF */ - if (ctx.srv.nid != test_cfg.srv.cipher) { + /* Verify: both should have the strongest cipher */ + if (ctx.srv.nid != NID_aes_256_gcm) { printf("Server cipher mismatch: expected %s, got %s\n", - crypt_nid_to_str(test_cfg.srv.cipher), + crypt_nid_to_str(NID_aes_256_gcm), crypt_nid_to_str(ctx.srv.nid)); goto fail_cleanup; } - if (ctx.cli.nid != test_cfg.srv.cipher) { + if (ctx.cli.nid != NID_aes_256_gcm) { printf("Client cipher mismatch: expected %s, got %s\n", - crypt_nid_to_str(test_cfg.srv.cipher), + crypt_nid_to_str(NID_aes_256_gcm), crypt_nid_to_str(ctx.cli.nid)); goto fail_cleanup; } @@ -598,9 +598,9 @@ static int test_oap_cipher_mismatch(void) /* KDF NID at offset 26: ID(16) + ts(8) + cipher(2) */ resp_kdf_nid = ntoh16(*(uint16_t *)(ctx.resp_hdr.data + 26)); - if (resp_kdf_nid != test_cfg.srv.kdf) { + if (resp_kdf_nid != NID_sha512) { printf("Response KDF mismatch: expected %s, got %s\n", - md_nid_to_str(test_cfg.srv.kdf), + md_nid_to_str(NID_sha512), md_nid_to_str(resp_kdf_nid)); goto fail_cleanup; } @@ -618,6 +618,228 @@ static int test_oap_cipher_mismatch(void) return TEST_RC_FAIL; } +/* Server encryption, client none: server rejects (no KEX data) */ +static int test_oap_srv_enc_cli_none(void) +{ + struct oap_test_ctx ctx; + + TEST_START(); + + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: encryption configured */ + test_cfg.srv.kex = NID_X25519; + test_cfg.srv.cipher = NID_aes_256_gcm; + test_cfg.srv.kdf = NID_sha256; + test_cfg.srv.md = NID_sha256; + test_cfg.srv.auth = AUTH; + + /* Client: no encryption */ + test_cfg.cli.md = NID_sha256; + test_cfg.cli.auth = NO_AUTH; + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + /* Server should reject: KEX required but client sent none */ + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should have rejected.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Client encryption, server none: use client settings */ +static int test_oap_cli_enc_srv_none(void) +{ + struct oap_test_ctx ctx; + + TEST_START(); + + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: no encryption configured */ + test_cfg.srv.md = NID_sha256; + test_cfg.srv.auth = AUTH; + + /* Client: encryption configured */ + test_cfg.cli.kex = NID_X25519; + test_cfg.cli.cipher = NID_aes_256_gcm; + test_cfg.cli.kdf = NID_sha256; + test_cfg.cli.md = NID_sha256; + test_cfg.cli.auth = NO_AUTH; + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + if (oap_cli_complete_ctx(&ctx) < 0) { + printf("Client complete failed.\n"); + goto fail_cleanup; + } + + if (memcmp(ctx.cli.key, ctx.srv.key, SYMMKEYSZ) != 0) { + printf("Key mismatch.\n"); + goto fail_cleanup; + } + + if (ctx.cli.nid != NID_aes_256_gcm) { + printf("Expected %s, got %s.\n", + crypt_nid_to_str(NID_aes_256_gcm), + crypt_nid_to_str(ctx.cli.nid)); + goto fail_cleanup; + } + + if (ctx.srv.nid != NID_aes_256_gcm) { + printf("Expected %s, got %s.\n", + crypt_nid_to_str(NID_aes_256_gcm), + crypt_nid_to_str(ctx.srv.nid)); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Client rejects server response with downgraded cipher */ +static int test_oap_cli_rejects_downgrade(void) +{ + struct oap_test_ctx ctx; + uint16_t weak; + + TEST_START(); + + memset(&test_cfg, 0, sizeof(test_cfg)); + + test_cfg.srv.kex = NID_X25519; + test_cfg.srv.cipher = NID_aes_256_gcm; + test_cfg.srv.kdf = NID_sha256; + test_cfg.srv.md = NID_sha256; + test_cfg.srv.auth = AUTH; + + test_cfg.cli.kex = NID_X25519; + test_cfg.cli.cipher = NID_aes_256_gcm; + test_cfg.cli.kdf = NID_sha256; + test_cfg.cli.md = NID_sha256; + test_cfg.cli.auth = NO_AUTH; + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + if (oap_srv_process_ctx(&ctx) < 0) { + printf("Server process failed.\n"); + goto fail_cleanup; + } + + /* Tamper: replace cipher NID with weaker one */ + weak = hton16(NID_aes_128_ctr); + memcpy(ctx.resp_hdr.data + OAP_CIPHER_NID_OFFSET, + &weak, sizeof(weak)); + + /* Client should reject the downgraded cipher */ + if (oap_cli_complete_ctx(&ctx) == 0) { + printf("Client accepted downgrade.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + +/* Server rejects client with weaker KEX algorithm */ +static int test_oap_srv_rejects_weak_kex(void) +{ + struct oap_test_ctx ctx; + + TEST_START(); + + memset(&test_cfg, 0, sizeof(test_cfg)); + + /* Server: secp521r1 (strong) */ + test_cfg.srv.kex = NID_secp521r1; + test_cfg.srv.cipher = NID_aes_256_gcm; + test_cfg.srv.kdf = NID_sha256; + test_cfg.srv.md = NID_sha256; + test_cfg.srv.auth = AUTH; + + /* Client: ffdhe2048 (weakest) */ + test_cfg.cli.kex = NID_ffdhe2048; + test_cfg.cli.cipher = NID_aes_256_gcm; + test_cfg.cli.kdf = NID_sha256; + test_cfg.cli.md = NID_sha256; + test_cfg.cli.auth = NO_AUTH; + + if (oap_test_setup(&ctx, root_ca_crt_ec, im_ca_crt_ec) < 0) + goto fail; + + if (oap_cli_prepare_ctx(&ctx) < 0) { + printf("Client prepare failed.\n"); + goto fail_cleanup; + } + + /* Server should reject: client KEX too weak */ + if (oap_srv_process_ctx(&ctx) == 0) { + printf("Server should reject weak KEX.\n"); + goto fail_cleanup; + } + + oap_test_teardown(&ctx); + + TEST_SUCCESS(); + return TEST_RC_SUCCESS; + + fail_cleanup: + oap_test_teardown(&ctx); + fail: + TEST_FAIL(); + return TEST_RC_FAIL; +} + /* Test roundtrip with different signature digest algorithms */ static int test_oap_roundtrip_md(int md) { @@ -682,8 +904,8 @@ static int test_oap_roundtrip_md_all(void) int ret = 0; int i; - /* Test with default (0) */ - ret |= test_oap_roundtrip_md(0); + /* Test with default */ + ret |= test_oap_roundtrip_md(NID_undef); /* Test with all supported digest NIDs */ for (i = 0; md_supported_nids[i] != NID_undef; i++) @@ -919,6 +1141,10 @@ int oap_test(int argc, ret |= test_oap_unsupported_nid(); ret |= test_oap_cipher_mismatch(); + ret |= test_oap_srv_enc_cli_none(); + ret |= test_oap_cli_enc_srv_none(); + ret |= test_oap_cli_rejects_downgrade(); + ret |= test_oap_srv_rejects_weak_kex(); ret |= test_oap_outdated_packet(); ret |= test_oap_future_packet(); @@ -940,6 +1166,10 @@ int oap_test(int argc, (void) test_oap_nid_without_kex; (void) test_oap_unsupported_nid; (void) test_oap_cipher_mismatch; + (void) test_oap_srv_enc_cli_none; + (void) test_oap_cli_enc_srv_none; + (void) test_oap_cli_rejects_downgrade; + (void) test_oap_srv_rejects_weak_kex; (void) test_oap_outdated_packet; (void) test_oap_future_packet; (void) test_oap_replay_packet; |
