// Copyright (c) 2026. NetFoundry Inc // // SPDX-License-Identifier: Apache-2.0 // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include "crypto.h" #include "ziti/ziti_log.h" #include struct e2ee_deleter { void operator()(e2ee_t *e) const { e->free(e); } }; static void test_e2ee(e2ee_t *alice, e2ee_t *bob) { auto alice_pub = alice->pub(alice); auto bob_pub = bob->pub(bob); REQUIRE(alice->init(alice, bob_pub.key, bob_pub.key_len, true) == 0); REQUIRE(bob->init(bob, alice_pub.key, alice_pub.key_len, false) == 0); uint8_t alice_header[E2EE_MAX_HEADER_LEN]; uint8_t bob_header[E2EE_MAX_HEADER_LEN]; auto alice_header_len = alice->get_header(alice, alice_header); auto bob_header_len = bob->get_header(bob, bob_header); REQUIRE(alice_header_len >= 0); REQUIRE(bob_header_len >= 0); uint8_t out[1024]; if (alice_header_len > 0) { REQUIRE(bob->decrypt(bob, alice_header, alice_header_len, out, sizeof(out)) == 0); } if (bob_header_len > 0) { REQUIRE(alice->decrypt(alice, bob_header, bob_header_len, out, sizeof(out)) == 0); } for (int i = 0; i < 10; i++) { for (auto test_case : {std::make_pair(alice, bob), std::make_pair(bob, alice)}) { auto sender = test_case.first; auto receiver = test_case.second; INFO("Testing: " << (bob == sender ? "Bob" : "Alice") << " -> " << (bob == receiver ? "Bob" : "Alice") << "(Round " << i << ")"); char plaintext[1024]; randombytes_buf(plaintext, sizeof(plaintext)); uint8_t ciphertext[1024 + 256]; auto ciphertext_len = sender->encrypt(sender, (uint8_t *)plaintext, sizeof(plaintext), ciphertext, sizeof(ciphertext)); REQUIRE(ciphertext_len > 0); char plaintext_recv[1024]; auto plaintext_recv_len = receiver->decrypt(receiver, ciphertext, ciphertext_len, (uint8_t *)plaintext_recv, sizeof(plaintext_recv)); REQUIRE(plaintext_recv_len == sizeof(plaintext)); REQUIRE(memcmp(plaintext, plaintext_recv, sizeof(plaintext)) == 0); } } } TEST_CASE("e2ee", "[crypto]") { ziti_log_init(nullptr, 5, nullptr); auto e2ee = GENERATE(ziti_crypto_none, ziti_crypto_libsodium, ziti_crypto_aes_gcm); WHEN("e2ee_impl_t: " << e2ee_method_id(e2ee)) { auto alice = std::unique_ptr(create_e2ee(e2ee)); auto bob = std::unique_ptr(create_e2ee(e2ee)); if (alice == nullptr || bob == nullptr) { SKIP("e2ee method " << e2ee_method_id(e2ee) << " not implemented, skipping"); } test_e2ee(alice.get(), bob.get()); } } TEST_CASE("e2ee libsodium init rejects wrong peer key length", "[crypto]") { auto e = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); uint8_t too_short[crypto_kx_PUBLICKEYBYTES - 1] = {0}; uint8_t too_long[crypto_kx_PUBLICKEYBYTES + 1] = {0}; REQUIRE(e->init(e.get(), too_short, sizeof(too_short), false) == -1); REQUIRE(e->init(e.get(), too_long, sizeof(too_long), false) == -1); REQUIRE(e->init(e.get(), nullptr, 0, false) == -1); } TEST_CASE("e2ee libsodium init is one-shot", "[crypto]") { auto alice = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); auto bob = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); auto bob_pub = bob->pub(bob.get()); REQUIRE(alice->init(alice.get(), bob_pub.key, bob_pub.key_len, false) == 0); REQUIRE(alice->init(alice.get(), bob_pub.key, bob_pub.key_len, false) == -1); } TEST_CASE("e2ee libsodium get_header is one-shot", "[crypto]") { auto alice = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); auto bob = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); auto bob_pub = bob->pub(bob.get()); REQUIRE(alice->init(alice.get(), bob_pub.key, bob_pub.key_len, false) == 0); uint8_t header[E2EE_MAX_HEADER_LEN]; REQUIRE(alice->get_header(alice.get(), header) > 0); REQUIRE(alice->get_header(alice.get(), header) == -1); } TEST_CASE("e2ee libsodium clone is independent of parent", "[crypto]") { // Models the bind.c listener pattern: a parent keypair is cloned per // accepted connection, and init() on the clone must not consume the // parent's secret key. auto listener = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); auto listener_pub = listener->pub(listener.get()); std::vector pub_snapshot(listener_pub.key, listener_pub.key + listener_pub.key_len); auto peer1 = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); auto peer1_pub = peer1->pub(peer1.get()); auto clone1 = std::unique_ptr(listener->clone(listener.get())); REQUIRE(clone1->init(clone1.get(), peer1_pub.key, peer1_pub.key_len, true) == 0); auto listener_pub_after = listener->pub(listener.get()); REQUIRE(listener_pub_after.key_len == pub_snapshot.size()); REQUIRE(memcmp(listener_pub_after.key, pub_snapshot.data(), pub_snapshot.size()) == 0); // listener still usable: second clone+init succeeds against a fresh peer auto peer2 = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); auto peer2_pub = peer2->pub(peer2.get()); auto clone2 = std::unique_ptr(listener->clone(listener.get())); REQUIRE(clone2->init(clone2.get(), peer2_pub.key, peer2_pub.key_len, true) == 0); } TEST_CASE("e2ee libsodium decrypt retries after partial header", "[crypto]") { auto alice = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); auto bob = std::unique_ptr(create_e2ee(ziti_crypto_libsodium)); auto alice_pub = alice->pub(alice.get()); auto bob_pub = bob->pub(bob.get()); REQUIRE(alice->init(alice.get(), bob_pub.key, bob_pub.key_len, true) == 0); REQUIRE(bob->init(bob.get(), alice_pub.key, alice_pub.key_len, false) == 0); uint8_t header[E2EE_MAX_HEADER_LEN]; auto header_len = alice->get_header(alice.get(), header); REQUIRE(header_len > 0); uint8_t out[1024]; // truncated header must fail without consuming the receiver's header state REQUIRE(bob->decrypt(bob.get(), header, (size_t)header_len - 1, out, sizeof(out)) == -1); // full header on retry must succeed REQUIRE(bob->decrypt(bob.get(), header, (size_t)header_len, out, sizeof(out)) == 0); // and the subsequent ciphertext round-trip still works uint8_t plaintext[64]; randombytes_buf(plaintext, sizeof(plaintext)); uint8_t ciphertext[sizeof(plaintext) + E2EE_MAX_MSG_OVERHEAD]; auto ct_len = alice->encrypt(alice.get(), plaintext, sizeof(plaintext), ciphertext, sizeof(ciphertext)); REQUIRE(ct_len > 0); auto pt_len = bob->decrypt(bob.get(), ciphertext, ct_len, out, sizeof(out)); REQUIRE(pt_len == sizeof(plaintext)); REQUIRE(memcmp(out, plaintext, sizeof(plaintext)) == 0); } #if _WIN32 namespace ossl { #include "../library/e2ee/e2ee_aes_gcm_ossl.c" } TEST_CASE("e2ee-ossl-wincrypto-interop", "[crypto]") { auto alice = std::unique_ptr(create_e2ee(ziti_crypto_aes_gcm)); auto bob = std::unique_ptr((e2ee_t*)ossl::new_aes_gcm_e2ee()); test_e2ee(alice.get(), bob.get()); } #endif #if defined(__APPLE__) namespace ossl { #include "../library/e2ee/e2ee_aes_gcm_ossl.c" } // the Apple backend derives keys with Security.framework + CommonCrypto, so this is what // proves it stays wire-compatible with the OpenSSL/BCrypt peers. Both orderings, because // the two directions use different HKDF info. TEST_CASE("e2ee-ossl-apple-interop", "[crypto]") { ziti_log_init(nullptr, 5, nullptr); WHEN("apple is the server") { auto alice = std::unique_ptr(create_e2ee(ziti_crypto_aes_gcm)); auto bob = std::unique_ptr((e2ee_t *)ossl::new_aes_gcm_e2ee()); test_e2ee(alice.get(), bob.get()); } WHEN("openssl is the server") { auto alice = std::unique_ptr((e2ee_t *)ossl::new_aes_gcm_e2ee()); auto bob = std::unique_ptr(create_e2ee(ziti_crypto_aes_gcm)); test_e2ee(alice.get(), bob.get()); } } // the ossl backend above defines the same tuning macros with different values #undef AES_GCM_TAG_LEN #undef AES_GCM_NONCE_LEN #undef AES_GCM_NONCE_PREFIX_LEN #undef AES_GCM_KEY_LEN #undef P256_PUB_KEY_LEN namespace apple { #include "../library/e2ee/e2ee_aes_gcm_apple.c" } // An empty payload reaches the CryptoKit shim as (NULL, 0), a different path through the // Swift bridging than a non-empty buffer -- and one that traps inside CryptoKit if the null // base pointer is passed straight through, where the CommonCrypto one-shot accepted it. // Note the receiving side rejects a tag-only frame (ciphertext_len <= AES_GCM_TAG_LEN); that // guard is pre-existing and identical in the OpenSSL backend, so this only pins encrypt. TEST_CASE("e2ee-apple-aes-gcm-empty-payload", "[crypto]") { auto alice = std::unique_ptr(create_e2ee(ziti_crypto_aes_gcm)); auto bob = std::unique_ptr(create_e2ee(ziti_crypto_aes_gcm)); auto alice_pub = alice->pub(alice.get()); auto bob_pub = bob->pub(bob.get()); REQUIRE(alice->init(alice.get(), bob_pub.key, bob_pub.key_len, true) == 0); REQUIRE(bob->init(bob.get(), alice_pub.key, alice_pub.key_len, false) == 0); // must not trap: produces a tag and nothing else uint8_t ciphertext[E2EE_MAX_MSG_OVERHEAD]; auto ct_len = alice->encrypt(alice.get(), nullptr, 0, ciphertext, sizeof(ciphertext)); REQUIRE(ct_len == 16); // and the receive side must not trap on the resulting tag-only frame either uint8_t out[16]; REQUIRE(bob->decrypt(bob.get(), ciphertext, ct_len, out, sizeof(out)) == -1); } // RFC 5869 test case 3: SHA-256, zero-length salt and info. The zero-length salt is exactly // what derive_session_keys() uses, so this pins the CCHmac-based HKDF independently of OpenSSL. TEST_CASE("e2ee-apple-hkdf-rfc5869", "[crypto]") { const uint8_t ikm[22] = { 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, }; const uint8_t expected[42] = { 0x8d, 0xa4, 0xe7, 0x75, 0xa5, 0x63, 0xc1, 0x8f, 0x71, 0x5f, 0x80, 0x2a, 0x06, 0x3c, 0x5a, 0x31, 0xb8, 0xa1, 0x1f, 0x5c, 0x5e, 0xe1, 0x87, 0x9e, 0xc3, 0x45, 0x4e, 0x5f, 0x3c, 0x73, 0x8d, 0x2d, 0x9d, 0x20, 0x13, 0x95, 0xfa, 0xa4, 0xb6, 0x1a, 0x96, 0xc8, }; const uint8_t info[1] = {0}; uint8_t out[sizeof(expected)]; REQUIRE(apple::hkdf_sha256(ikm, sizeof(ikm), info, 0, out, sizeof(out)) == 0); REQUIRE(memcmp(out, expected, sizeof(expected)) == 0); // more than 255 blocks of output is out of range for HKDF REQUIRE(apple::hkdf_sha256(ikm, sizeof(ikm), info, 0, out, 255 * 32 + 1) == -1); } #endif