blob: 48ca47e0f8683de4c96473a90a2b1cb0222cb314 [file] [edit]
// 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 <assert.h>
#include <stdint.h>
#include <string.h>
#include <openssl/base.h>
#include <openssl/bytestring.h>
#include <openssl/curve25519.h>
#include <openssl/ec.h>
#include <openssl/err.h>
#include <openssl/mem.h>
#include <openssl/rand.h>
#include <openssl/sha2.h>
#include <openssl/span.h>
#include "../internal.h"
#include "../mem_internal.h"
#include "./internal.h"
CPACE_CTX *CPACE_CTX_new(enum cpace_role_t role, const uint8_t *password,
size_t password_len, const uint8_t *channel_id,
size_t channel_id_len, const uint8_t *assoc_data,
size_t assoc_data_len, const uint8_t *session_id,
size_t session_id_len) {
bssl::UniquePtr<bssl::CpaceCtx> ctx = bssl::MakeUnique<bssl::CpaceCtx>();
if (ctx == nullptr) {
return nullptr;
}
ctx->our_role = role;
if (!ctx->our_password.CopyFrom(bssl::Span(password, password_len)) ||
!ctx->channel_id.CopyFrom(bssl::Span(channel_id, channel_id_len)) ||
!ctx->our_aad.CopyFrom(bssl::Span(assoc_data, assoc_data_len)) ||
!ctx->session_id.CopyFrom(bssl::Span(session_id, session_id_len))) {
return nullptr;
}
return ctx.release();
}
void CPACE_CTX_free(CPACE_CTX *ctx) { Delete(bssl::FromOpaque(ctx)); }
namespace {
constexpr uint64_t leb128_len(uint64_t len) {
if (len < 128) {
return 1;
}
return (bssl::CRYPTO_bit_width(len) + 6) / 7;
}
constexpr size_t kMaxLenTagSize = leb128_len(UINT64_MAX);
constexpr std::string_view kDSI = "CPace255";
constexpr size_t leb128_tag(uint64_t len, uint8_t tag[kMaxLenTagSize]) {
size_t tag_len = leb128_len(len);
for (size_t i = 0; i < tag_len; ++i) {
if (len < 128) {
tag[i] = len;
} else {
tag[i] = (len & 0x7f) | 0x80;
len >>= 7;
}
}
return tag_len;
}
void push_lv_to_hasher(SHA512_CTX &hasher, bssl::Span<const uint8_t> data) {
uint8_t lv_tag[kMaxLenTagSize];
size_t lv_tag_len = leb128_tag(data.size(), lv_tag);
SHA512_Update(&hasher, lv_tag, lv_tag_len);
SHA512_Update(&hasher, data.data(), data.size());
}
} // namespace
// This implements the first two steps of G.calculate_generator in Section 8.2
// of draft-irtf-cfrg-cpace-21.
void bssl::CpaceCtx::ComputeGeneratorStrHash(
uint8_t out_gen_str_hash[32]) const {
// len_zpad = max(0,s_in_bytes - 1 - len(prepend_len(PRS)) -
// len(prepend_len(DSI)))
size_t digest_len = leb128_len(our_password.size()) + our_password.size() //
+ leb128_len(kDSI.length()) + kDSI.length();
size_t len_zpad = 0;
if (digest_len < SHA512_CBLOCK - 1) {
len_zpad = SHA512_CBLOCK - 1 - digest_len;
}
constexpr uint8_t kZeros[SHA512_CBLOCK] = {};
// gen_str = generator_string(G.DSI, PRS, CI, sid, H.s_in_bytes)
SHA512_CTX hasher;
SHA512_Init(&hasher);
push_lv_to_hasher(hasher, StringAsBytes(kDSI));
push_lv_to_hasher(hasher, our_password);
push_lv_to_hasher(hasher, Span(kZeros, len_zpad));
push_lv_to_hasher(hasher, channel_id);
push_lv_to_hasher(hasher, session_id);
uint8_t out[SHA512_DIGEST_LENGTH];
// gen_str_hash = H.hash(gen_str, G.field_size_bytes).
SHA512_Final(out, &hasher);
OPENSSL_memcpy(out_gen_str_hash, out, 32);
}
bool bssl::CpaceCtx::ComputeMessage(uint8_t msg[CPACE_MSG_LEN]) {
if (state != cpace_state_init) {
OPENSSL_PUT_ERROR(CRYPTO, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return false;
}
uint8_t gen_str_hash[32];
// We now compute the first two steps of G.calculate_generator(H, PRS, sid,
// CI).
ComputeGeneratorStrHash(gen_str_hash);
// This result is then considered as a field coordinate using the u =
// decodeUCoordinate(gen_str_hash, G.field_size_bits) function from RFC7748.
uint8_t g[32], v[32];
map_to_curve_curve25519_elligator2(g, v, gen_str_hash);
// sample_scalar() = sample_random_bytes(G.field_size_bytes)
RAND_bytes(our_private_key, 32);
auto rc = X25519(our_public_key, our_private_key, g);
OPENSSL_memcpy(msg, our_public_key, 32);
if (!rc) {
// This will happen with negligible probability.
OPENSSL_PUT_ERROR(CRYPTO, ERR_R_INTERNAL_ERROR);
return false;
}
state = cpace_state_msg_generated;
return true;
}
int CPACE_generate_msg(CPACE_CTX *ctx, uint8_t out_msg[CPACE_MSG_LEN]) {
auto *impl = bssl::FromOpaque(ctx);
return impl->ComputeMessage(out_msg);
}
bool bssl::CpaceCtx::ComputeISK(
const uint8_t peer_msg[CPACE_MSG_LEN], Span<const uint8_t> peer_assoc_data,
uint8_t out_shared_secret[CPACE_SHARED_SECRET_LEN],
uint8_t out_sid[SHA512_DIGEST_LENGTH]) {
if (state != cpace_state_msg_generated) {
OPENSSL_PUT_ERROR(CRYPTO, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
return false;
}
state = cpace_state_key_generated;
uint8_t k[32];
if (X25519(k, our_private_key, peer_msg) != 1) {
// TODO(crbug.com/42290066): This is another instance where unifying the
// reason code and library code into one error code makes most sense.
OPENSSL_PUT_ERROR(EC, EC_R_DECODE_ERROR);
return false;
}
// ISK = H.hash(lv_cat(G.DSI || b"_ISK", sid, K)||transcript(Ya,ADa,Yb,ADb))
static constexpr std::string_view kDsiIsk = "CPace255_ISK";
static constexpr std::string_view kDsiSidOut = "CPaceSidOutput";
SHA512_CTX hasher, sid_hasher;
SHA512_Init(&hasher);
const bool compute_sid = out_sid != nullptr;
if (compute_sid) {
SHA512_Init(&sid_hasher);
SHA512_Update(&sid_hasher, kDsiSidOut.data(), kDsiSidOut.size());
}
push_lv_to_hasher(hasher, StringAsBytes(kDsiIsk));
push_lv_to_hasher(hasher, session_id);
push_lv_to_hasher(hasher, Span(k, 32));
Span peer_public_key(peer_msg, 32);
auto push_our_transcript = [&] {
push_lv_to_hasher(hasher, our_public_key);
push_lv_to_hasher(hasher, our_aad);
if (compute_sid) {
push_lv_to_hasher(sid_hasher, our_public_key);
push_lv_to_hasher(sid_hasher, our_aad);
}
};
auto push_peer_transcript = [&] {
push_lv_to_hasher(hasher, peer_public_key);
push_lv_to_hasher(hasher, peer_assoc_data);
if (compute_sid) {
push_lv_to_hasher(sid_hasher, peer_public_key);
push_lv_to_hasher(sid_hasher, peer_assoc_data);
}
};
switch (our_role) {
case cpace_role_initiator: {
push_our_transcript();
push_peer_transcript();
break;
}
case cpace_role_responder: {
push_peer_transcript();
push_our_transcript();
break;
}
}
if (compute_sid) {
SHA512_Final(out_sid, &sid_hasher);
}
return SHA512_Final(out_shared_secret, &hasher);
}
// CPACE_process_msg processes a received message and finalize
int CPACE_process_msg(CPACE_CTX *ctx, const uint8_t msg[CPACE_MSG_LEN],
const uint8_t *peer_assoc_data,
size_t peer_assoc_data_len,
uint8_t out_shared_secret[CPACE_SHARED_SECRET_LEN],
uint8_t out_sid[SHA512_DIGEST_LENGTH]) {
auto *impl = bssl::FromOpaque(ctx);
return impl->ComputeISK(msg, bssl::Span(peer_assoc_data, peer_assoc_data_len),
out_shared_secret, out_sid);
}