| // Copyright 2026 The BoringSSL Authors |
| // |
| // 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 <openssl/curve25519.h> |
| #include <openssl/span.h> |
| |
| #include <optional> |
| #include <utility> |
| #include <vector> |
| |
| #include <stdint.h> |
| #include <string.h> |
| |
| #include <gtest/gtest.h> |
| |
| #include "../internal.h" |
| #include "../test/test_util.h" |
| #include "./internal.h" |
| |
| |
| BSSL_NAMESPACE_BEGIN |
| namespace { |
| |
| static constexpr uint8_t kPassword[] = {'P', 'a', 's', 's', 'w', 'o', 'r', 'd'}; |
| static constexpr uint8_t kChannelId[] = { |
| 0x0b, 'A', '_', 'i', 'n', 'i', 't', 'i', 'a', 't', 'o', 'r', |
| 0x0b, 'B', '_', 'r', 'e', 's', 'p', 'o', 'n', 'd', 'e', 'r'}; |
| static constexpr uint8_t kAda[] = {'A', 'D', 'a'}; |
| static constexpr uint8_t kAdb[] = {'A', 'D', 'b'}; |
| static constexpr uint8_t kSid[] = {0x7e, 0x4b, 0x47, 0x91, 0xd6, 0xa8, |
| 0xef, 0x01, 0x9b, 0x93, 0x6c, 0x79, |
| 0xfb, 0x7f, 0x2c, 0x57}; |
| |
| struct CPACERun { |
| bool Run() { |
| UniquePtr<CPACE_CTX> alice(CPACE_CTX_new( |
| cpace_role_initiator, alice_password.data(), alice_password.size(), |
| alice_channel_id.data(), alice_channel_id.size(), alice_aad.data(), |
| alice_aad.size(), alice_sid.data(), alice_sid.size())); |
| UniquePtr<CPACE_CTX> bob(CPACE_CTX_new( |
| cpace_role_responder, bob_password.data(), bob_password.size(), |
| bob_channel_id.data(), bob_channel_id.size(), bob_aad.data(), |
| bob_aad.size(), bob_sid.data(), bob_sid.size())); |
| |
| if (!alice || !bob) { |
| return false; |
| } |
| |
| uint8_t alice_msg[CPACE_MSG_LEN]; |
| uint8_t bob_msg[CPACE_MSG_LEN]; |
| |
| if (!CPACE_generate_msg(alice.get(), alice_msg) || |
| !CPACE_generate_msg(bob.get(), bob_msg)) { |
| return false; |
| } |
| |
| if (alice_corrupt_msg_bit >= 0 && |
| static_cast<size_t>(alice_corrupt_msg_bit) < 8 * sizeof(alice_msg)) { |
| alice_msg[alice_corrupt_msg_bit / 8] ^= 1 << (alice_corrupt_msg_bit & 7); |
| } |
| |
| Span<const uint8_t> alice_recv_aad = |
| alice_peer_aad.has_value() ? *alice_peer_aad : bob_aad; |
| Span<const uint8_t> bob_recv_aad = |
| bob_peer_aad.has_value() ? *bob_peer_aad : alice_aad; |
| |
| uint8_t alice_key[64], bob_key[64]; |
| uint8_t alice_sid_out[SHA512_DIGEST_LENGTH], |
| bob_sid_out[SHA512_DIGEST_LENGTH]; |
| if (!CPACE_process_msg(alice.get(), bob_msg, alice_recv_aad.data(), |
| alice_recv_aad.size(), alice_key, |
| pass_sid_out ? alice_sid_out : nullptr) || |
| !CPACE_process_msg(bob.get(), alice_msg, bob_recv_aad.data(), |
| bob_recv_aad.size(), bob_key, |
| pass_sid_out ? bob_sid_out : nullptr)) { |
| return false; |
| } |
| |
| key_matches_ = (CRYPTO_memcmp(alice_key, bob_key, sizeof(alice_key)) == 0); |
| sid_matches_ = !pass_sid_out || (CRYPTO_memcmp(alice_sid_out, bob_sid_out, |
| sizeof(alice_sid_out)) == 0); |
| return true; |
| } |
| |
| bool key_matches() const { return key_matches_; } |
| bool sid_matches() const { return sid_matches_; } |
| |
| Span<const uint8_t> alice_password = kPassword; |
| Span<const uint8_t> bob_password = kPassword; |
| Span<const uint8_t> alice_channel_id = kChannelId; |
| Span<const uint8_t> bob_channel_id = kChannelId; |
| Span<const uint8_t> alice_aad = kAda; |
| Span<const uint8_t> bob_aad = kAdb; |
| std::optional<Span<const uint8_t>> alice_peer_aad; |
| std::optional<Span<const uint8_t>> bob_peer_aad; |
| Span<const uint8_t> alice_sid = kSid; |
| Span<const uint8_t> bob_sid = kSid; |
| bool pass_sid_out = true; |
| int alice_corrupt_msg_bit = -1; |
| |
| private: |
| bool key_matches_ = false; |
| bool sid_matches_ = false; |
| }; |
| |
| TEST(CPACETest, Handshake) { |
| for (int i = 0; i < 10; i++) { |
| CPACERun run; |
| ASSERT_TRUE(run.Run()); |
| EXPECT_TRUE(run.key_matches()); |
| EXPECT_TRUE(run.sid_matches()); |
| } |
| } |
| |
| TEST(CPACETest, HandshakeNullSidOut) { |
| CPACERun run; |
| run.pass_sid_out = false; |
| ASSERT_TRUE(run.Run()); |
| EXPECT_TRUE(run.key_matches()); |
| } |
| |
| TEST(CPACETest, HandshakeVariableAad) { |
| std::vector<uint8_t> aad127(127, 0x11); |
| std::vector<uint8_t> aad128(128, 0x22); |
| std::vector<uint8_t> aad255(255, 0x33); |
| std::vector<uint8_t> aad256(256, 0x44); |
| std::vector<uint8_t> aad300_0(300, 0x00); |
| std::vector<uint8_t> aad300_1(300, 0x01); |
| static constexpr uint8_t kA[] = {'A'}; |
| static constexpr uint8_t kB[] = {'B'}; |
| static constexpr uint8_t kShort[] = {'s', 'h', 'o', 'r', 't'}; |
| static constexpr uint8_t kLong[] = {'l', 'o', 'n', 'g', 'e', 'r'}; |
| |
| std::vector<std::pair<Span<const uint8_t>, Span<const uint8_t>>> aad_pairs = { |
| {{}, {}}, |
| {kA, kB}, |
| {kB, kA}, |
| {kShort, kLong}, |
| {aad127, aad128}, |
| {aad255, aad256}, |
| {aad300_0, aad300_1}, |
| }; |
| for (const auto &[alice_aad, bob_aad] : aad_pairs) { |
| CPACERun run; |
| run.alice_aad = alice_aad; |
| run.bob_aad = bob_aad; |
| ASSERT_TRUE(run.Run()); |
| EXPECT_TRUE(run.key_matches()); |
| EXPECT_TRUE(run.sid_matches()); |
| } |
| } |
| |
| TEST(CPACETest, EmptySidAndAad) { |
| CPACERun run; |
| run.alice_sid = {}; |
| run.bob_sid = {}; |
| run.alice_aad = {}; |
| run.bob_aad = {}; |
| ASSERT_TRUE(run.Run()); |
| EXPECT_TRUE(run.key_matches()); |
| EXPECT_TRUE(run.sid_matches()); |
| } |
| |
| TEST(CPACETest, WrongPassword) { |
| static constexpr uint8_t kWrongPassword[] = {'W', 'r', 'o', 'n', 'g'}; |
| CPACERun run; |
| run.bob_password = kWrongPassword; |
| ASSERT_TRUE(run.Run()); |
| EXPECT_FALSE(run.key_matches()) << "Keys matched with different passwords!"; |
| EXPECT_TRUE(run.sid_matches()); |
| } |
| |
| TEST(CPACETest, WrongChannelId) { |
| static constexpr uint8_t kWrongChannelId[] = {'W', 'r', 'o', 'n', |
| 'g', 'C', 'I'}; |
| CPACERun run; |
| run.bob_channel_id = kWrongChannelId; |
| ASSERT_TRUE(run.Run()); |
| EXPECT_FALSE(run.key_matches()) << "Keys matched with different channel IDs!"; |
| EXPECT_TRUE(run.sid_matches()); |
| } |
| |
| TEST(CPACETest, WrongSessionId) { |
| static constexpr uint8_t kWrongSessionId[] = {'O', 't', 'h', 'e', |
| 'r', 'S', 'i', 'd'}; |
| CPACERun run; |
| run.bob_sid = kWrongSessionId; |
| ASSERT_TRUE(run.Run()); |
| EXPECT_FALSE(run.key_matches()) << "Keys matched with different session IDs!"; |
| EXPECT_TRUE(run.sid_matches()); |
| } |
| |
| TEST(CPACETest, WrongAssociatedData) { |
| static constexpr uint8_t kTamperedAad[] = {'B', 'l', 'a', 'h'}; |
| CPACERun run; |
| run.alice_peer_aad = kTamperedAad; |
| ASSERT_TRUE(run.Run()); |
| EXPECT_FALSE(run.key_matches()) |
| << "Keys matched with tampered associated data!"; |
| EXPECT_FALSE(run.sid_matches()); |
| } |
| |
| TEST(CPACETest, CorruptedMessage) { |
| for (int i = 0; i < 8 * 32; i += 7) { |
| CPACERun run; |
| run.alice_corrupt_msg_bit = i; |
| // Corrupted message should either fail during X25519 (if point of small |
| // order) or produce non-matching keys. |
| if (run.Run()) { |
| EXPECT_FALSE(run.key_matches()) |
| << "Key matched after corrupting Alice's message, bit " << i; |
| EXPECT_FALSE(run.sid_matches()) |
| << "SID matched after corrupting Alice's message, bit " << i; |
| } |
| } |
| } |
| |
| TEST(CPACETest, StateMachine) { |
| uint8_t msg[32]; |
| uint8_t key[64]; |
| uint8_t sid[SHA512_DIGEST_LENGTH]; |
| |
| UniquePtr<CPACE_CTX> ctx(CPACE_CTX_new( |
| cpace_role_initiator, kPassword, sizeof(kPassword), kChannelId, |
| sizeof(kChannelId), kAda, sizeof(kAda), nullptr, 0)); |
| ASSERT_TRUE(ctx); |
| |
| // Cannot process before generating message |
| uint8_t dummy_peer_msg[32] = {0}; |
| EXPECT_FALSE( |
| CPACE_process_msg(ctx.get(), dummy_peer_msg, nullptr, 0, key, sid)); |
| |
| // Generate message |
| EXPECT_TRUE(CPACE_generate_msg(ctx.get(), msg)); |
| |
| // Cannot send message again |
| EXPECT_FALSE(CPACE_generate_msg(ctx.get(), msg)); |
| |
| // Process message from peer (valid point) |
| uint8_t valid_peer_scalar[32] = {1}; |
| uint8_t valid_peer_msg[32]; |
| X25519_public_from_private(valid_peer_msg, valid_peer_scalar); |
| EXPECT_TRUE( |
| CPACE_process_msg(ctx.get(), valid_peer_msg, nullptr, 0, key, sid)); |
| |
| // Cannot process message again (single-use) |
| EXPECT_FALSE( |
| CPACE_process_msg(ctx.get(), valid_peer_msg, nullptr, 0, key, sid)); |
| } |
| |
| void RunExchange(Span<const uint8_t> ya_priv, Span<const uint8_t> yb_priv, |
| Span<const uint8_t> ya_msg, Span<const uint8_t> yb_msg, |
| Span<const uint8_t> expected_isk, |
| Span<const uint8_t> expected_sid_out, |
| Span<const uint8_t> sid = kSid) { |
| UniquePtr<CPACE_CTX> alice(CPACE_CTX_new( |
| cpace_role_initiator, kPassword, sizeof(kPassword), kChannelId, |
| sizeof(kChannelId), kAda, sizeof(kAda), sid.data(), sid.size())); |
| UniquePtr<CPACE_CTX> bob(CPACE_CTX_new( |
| cpace_role_responder, kPassword, sizeof(kPassword), kChannelId, |
| sizeof(kChannelId), kAdb, sizeof(kAdb), sid.data(), sid.size())); |
| ASSERT_TRUE(alice); |
| ASSERT_TRUE(bob); |
| |
| auto *alice_impl = FromOpaque(alice.get()); |
| auto *bob_impl = FromOpaque(bob.get()); |
| |
| if (!ya_priv.empty()) { |
| OPENSSL_memcpy(alice_impl->our_private_key, ya_priv.data(), 32); |
| } |
| OPENSSL_memcpy(alice_impl->our_public_key, ya_msg.data(), 32); |
| alice_impl->state = CpaceCtx::cpace_state_msg_generated; |
| |
| if (!yb_priv.empty()) { |
| OPENSSL_memcpy(bob_impl->our_private_key, yb_priv.data(), 32); |
| } |
| OPENSSL_memcpy(bob_impl->our_public_key, yb_msg.data(), 32); |
| bob_impl->state = CpaceCtx::cpace_state_msg_generated; |
| |
| uint8_t alice_isk[64], bob_isk[64]; |
| uint8_t alice_sid_out[SHA512_DIGEST_LENGTH], |
| bob_sid_out[SHA512_DIGEST_LENGTH]; |
| ASSERT_TRUE(CPACE_process_msg(alice.get(), yb_msg.data(), kAdb, sizeof(kAdb), |
| alice_isk, alice_sid_out)); |
| ASSERT_TRUE(CPACE_process_msg(bob.get(), ya_msg.data(), kAda, sizeof(kAda), |
| bob_isk, bob_sid_out)); |
| |
| if (!expected_isk.empty()) { |
| EXPECT_EQ(Bytes(alice_isk), Bytes(expected_isk)); |
| EXPECT_EQ(Bytes(bob_isk), Bytes(expected_isk)); |
| } |
| EXPECT_EQ(Bytes(alice_sid_out), Bytes(bob_sid_out)); |
| if (!expected_sid_out.empty()) { |
| EXPECT_EQ(Bytes(alice_sid_out), Bytes(expected_sid_out)); |
| EXPECT_EQ(Bytes(bob_sid_out), Bytes(expected_sid_out)); |
| } |
| } |
| |
| // Test vector from draft-irtf-cfrg-cpace Appendix B.1.1 - B.1.5 |
| TEST(CPACETest, RFCVectors) { |
| std::vector<uint8_t> ya, yb, ya_msg, yb_msg, expected_isk_ordered; |
| ASSERT_TRUE( |
| DecodeHex(&ya, |
| "21b4f4bd9e64ed355c3eb676a28ebedaf6d8f17bdc365995b3190971" |
| "53044080")); |
| ASSERT_TRUE( |
| DecodeHex(&yb, |
| "848b0779ff415f0af4ea14df9dd1d3c29ac41d836c7808896c4eba19" |
| "c51ac40a")); |
| ASSERT_TRUE( |
| DecodeHex(&ya_msg, |
| "1d13c89278cdadd826f6d8d7f887701430f8380ddc17611cdd6dc989" |
| "ce0c9f32")); |
| ASSERT_TRUE( |
| DecodeHex(&yb_msg, |
| "248cccf6d5cdc3646f0ad593f9e6cef4e69d4945f8372e623512ecea" |
| "32185623")); |
| ASSERT_TRUE( |
| DecodeHex(&expected_isk_ordered, |
| "6e19b875f7a561d6b3ca3dbb9ef42ac55de3e717881018204b8922b4" |
| "d5e53bb2aa82c300bea7b65d2b671da71922ddf6472301b79bc270ad" |
| "fa8bf413285f2263")); |
| |
| #if !defined(BORINGSSL_SHARED_LIBRARY) |
| std::vector<uint8_t> expected_g; |
| ASSERT_TRUE( |
| DecodeHex(&expected_g, |
| "d04bf6d41f6a289632a2e929fa29bebd51092512a7829fdde7d314b6" |
| "2f05a73f")); |
| |
| // 1. Generator and message derivation (Appendix B.1.1 - B.1.3) |
| UniquePtr<CPACE_CTX> ctx(CPACE_CTX_new( |
| cpace_role_initiator, kPassword, sizeof(kPassword), kChannelId, |
| sizeof(kChannelId), nullptr, 0, kSid, sizeof(kSid))); |
| ASSERT_TRUE(ctx); |
| auto *impl = FromOpaque(ctx.get()); |
| |
| uint8_t u[32], g[32], v[32]; |
| impl->ComputeGeneratorStrHash(u); |
| map_to_curve_curve25519_elligator2(g, v, u); |
| EXPECT_EQ(Bytes(g), Bytes(expected_g)); |
| |
| uint8_t computed_ya_msg[32], computed_yb_msg[32]; |
| ASSERT_TRUE(X25519(computed_ya_msg, ya.data(), g)); |
| EXPECT_EQ(Bytes(computed_ya_msg), Bytes(ya_msg)); |
| ASSERT_TRUE(X25519(computed_yb_msg, yb.data(), g)); |
| EXPECT_EQ(Bytes(computed_yb_msg), Bytes(yb_msg)); |
| #endif |
| |
| // 2. Exchange (Appendix B.1.5) |
| RunExchange(ya, yb, ya_msg, yb_msg, expected_isk_ordered, {}); |
| } |
| |
| // Test vector from draft-irtf-cfrg-cpace Appendix B.1.7 (Optional output of |
| // session id) |
| TEST(CPACETest, RFCSidOutput) { |
| std::vector<uint8_t> Ya, Yb, expected_sid_out_ir; |
| ASSERT_TRUE( |
| DecodeHex(&Ya, |
| "1d13c89278cdadd826f6d8d7f887701430f8380ddc17611cdd6dc989" |
| "ce0c9f32")); |
| ASSERT_TRUE( |
| DecodeHex(&Yb, |
| "248cccf6d5cdc3646f0ad593f9e6cef4e69d4945f8372e623512ecea" |
| "32185623")); |
| ASSERT_TRUE( |
| DecodeHex(&expected_sid_out_ir, |
| "cbc73f62589bbc96ab6a95ec2363df621e93bc3b0cea83ba6b9571d0" |
| "05fa8f5d2d08f7165622777fa484c02a9e6b20a84ee2dbebae8c53be" |
| "757dcfc0eebdeb5f")); |
| |
| // transcript_ir with empty sid |
| RunExchange({}, {}, Ya, Yb, {}, expected_sid_out_ir, {}); |
| } |
| |
| // Test vector from draft-irtf-cfrg-cpace Appendix B.1.10 (low order points) |
| TEST(CPACETest, RFCLowOrderPoints) { |
| struct LowOrderVector { |
| const char *u_hex; |
| const char *q_hex; |
| bool should_abort; |
| }; |
| |
| const LowOrderVector kVectors[] = { |
| {"0000000000000000000000000000000000000000000000000000000000000000", |
| "0000000000000000000000000000000000000000000000000000000000000000", |
| true}, |
| {"0100000000000000000000000000000000000000000000000000000000000000", |
| "0000000000000000000000000000000000000000000000000000000000000000", |
| true}, |
| {"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", |
| "0000000000000000000000000000000000000000000000000000000000000000", |
| true}, |
| {"e0eb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b800", |
| "0000000000000000000000000000000000000000000000000000000000000000", |
| true}, |
| {"5f9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f1157", |
| "0000000000000000000000000000000000000000000000000000000000000000", |
| true}, |
| {"edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", |
| "0000000000000000000000000000000000000000000000000000000000000000", |
| true}, |
| {"daffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", |
| "d8e2c776bbacd510d09fd9278b7edcd25fc5ae9adfba3b6e040e8d3b71b21806", |
| false}, |
| {"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", |
| "0000000000000000000000000000000000000000000000000000000000000000", |
| true}, |
| {"dbffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", |
| "c85c655ebe8be44ba9c0ffde69f2fe10194458d137f09bbff725ce58803cdb38", |
| false}, |
| {"d9ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", |
| "db64dafa9b8fdd136914e61461935fe92aa372cb056314e1231bc4ec12417456", |
| false}, |
| {"cdeb7a7c3b41b8ae1656e3faf19fc46ada098deb9c32b1fd866205165f49b880", |
| "e062dcd5376d58297be2618c7498f55baa07d7e03184e8aada20bca28888bf7a", |
| false}, |
| {"4c9c95bca3508c24b1d0b1559c83ef5b04445cc4581c8e86d8224eddd09f11d7", |
| "993c6ad11c4c29da9a56f7691fd0ff8d732e49de6250b6c2e80003ff4629a175", |
| false}, |
| }; |
| |
| std::vector<uint8_t> s; |
| ASSERT_TRUE(DecodeHex( |
| &s, "af46e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449aff")); |
| |
| for (size_t i = 0; i < std::size(kVectors); i++) { |
| SCOPED_TRACE(i); |
| const auto &vec = kVectors[i]; |
| std::vector<uint8_t> u, expected_q; |
| ASSERT_TRUE(DecodeHex(&u, vec.u_hex)); |
| ASSERT_TRUE(DecodeHex(&expected_q, vec.q_hex)); |
| |
| // Protocol abort verification: when included in message from peer, |
| // `CPACE_process_msg` must abort if `should_abort` is true. |
| UniquePtr<CPACE_CTX> ctx( |
| CPACE_CTX_new(cpace_role_initiator, kPassword, sizeof(kPassword), |
| kChannelId, sizeof(kChannelId), nullptr, 0, nullptr, 0)); |
| ASSERT_TRUE(ctx); |
| |
| uint8_t our_msg[CPACE_MSG_LEN]; |
| ASSERT_TRUE(CPACE_generate_msg(ctx.get(), our_msg)); |
| |
| uint8_t shared_secret[CPACE_SHARED_SECRET_LEN]; |
| uint8_t sid_out[SHA512_DIGEST_LENGTH]; |
| int process_res = CPACE_process_msg(ctx.get(), u.data(), nullptr, 0, |
| shared_secret, sid_out); |
| EXPECT_EQ(process_res, !vec.should_abort); |
| } |
| } |
| |
| } // namespace |
| BSSL_NAMESPACE_END |