blob: c63d9f99c67a59632b357fa3a685a1d4661b1f4a [file]
// 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