Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/core/crypto/crypto_helpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ inline auto curve_field_bytes(const EllipticCurve curve) noexcept
std::unreachable();
}

// The order bit length of each curve, which the platform key generators take as
// the requested key size. It is not the field width in bits, since P-521 has a
// 521-bit order that does not fill its 66 octets
inline auto curve_bit_length(const EllipticCurve curve) noexcept
-> std::size_t {
switch (curve) {
case EllipticCurve::P256:
return 256;
case EllipticCurve::P384:
return 384;
case EllipticCurve::P521:
return 521;
}

std::unreachable();
}

// The inverse mapping, identifying the curve from its field width, so a backend
// that reports a key only by coordinate size resolves it the same way
inline auto ec_curve_from_field_bytes(const std::size_t field_bytes) noexcept
Expand Down
35 changes: 35 additions & 0 deletions src/core/crypto/crypto_sign_apple.cc
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,41 @@ auto make_ec_private_key(const EllipticCurve curve,
.edwards_curve = {}}};
}

auto generate_ec_private_key(const EllipticCurve curve)
-> std::optional<PrivateKey> {
const int bits{static_cast<int>(curve_bit_length(curve))};
auto size{CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &bits)};
if (size == nullptr) {
return std::nullopt;
}

std::array<const void *, 2> attribute_keys{
{kSecAttrKeyType, kSecAttrKeySizeInBits}};
std::array<const void *, 2> attribute_values{
{kSecAttrKeyTypeECSECPrimeRandom, size}};
auto attributes{CFDictionaryCreate(
kCFAllocatorDefault, attribute_keys.data(), attribute_values.data(),
static_cast<CFIndex>(attribute_keys.size()),
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)};
CFRelease(size);
if (attributes == nullptr) {
return std::nullopt;
}

auto *key{SecKeyCreateRandomKey(attributes, nullptr)};
CFRelease(attributes);
if (key == nullptr) {
return std::nullopt;
}

return PrivateKey{
new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: If new PrivateKey::Internal{...} throws (e.g., std::bad_alloc), the algorithm and key handles have not yet been transferred into the PrivateKey destructor's ownership, so both platform handles will leak. Consider using a unique_ptr with a custom deleter or a local RAII guard to ensure cleanup on all exit paths. The same pattern also applies in the Apple and OpenSSL backends.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/core/crypto/crypto_sign_apple.cc, line 388:

<comment>If `new PrivateKey::Internal{...}` throws (e.g., `std::bad_alloc`), the `algorithm` and `key` handles have not yet been transferred into the `PrivateKey` destructor's ownership, so both platform handles will leak. Consider using a `unique_ptr` with a custom deleter or a local RAII guard to ensure cleanup on all exit paths. The same pattern also applies in the Apple and OpenSSL backends.</comment>

<file context>
@@ -357,6 +357,41 @@ auto make_ec_private_key(const EllipticCurve curve,
+  }
+
+  return PrivateKey{
+      new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve,
+                               .key = key,
+                               .field_bytes = curve_field_bytes(curve),
</file context>

.key = key,
.field_bytes = curve_field_bytes(curve),
.edwards_seed = {},
.edwards_curve = {}}};
}

auto make_edwards_private_key(const EdwardsCurve curve,
const std::string_view seed)
-> std::optional<PrivateKey> {
Expand Down
13 changes: 13 additions & 0 deletions src/core/crypto/crypto_sign_openssl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,19 @@ auto make_ec_private_key(const EllipticCurve curve,
.field_bytes = curve_field_bytes(curve)}};
}

auto generate_ec_private_key(const EllipticCurve curve)
-> std::optional<PrivateKey> {
auto *key{EVP_EC_gen(to_group_name(curve))};
if (key == nullptr) {
return std::nullopt;
}

return PrivateKey{
new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve,
.key = key,
.field_bytes = curve_field_bytes(curve)}};
}

auto make_edwards_private_key(const EdwardsCurve curve,
const std::string_view seed)
-> std::optional<PrivateKey> {
Expand Down
45 changes: 45 additions & 0 deletions src/core/crypto/crypto_sign_other.cc
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@
#include "crypto_helpers.h"
#include "crypto_other.h"
#include "crypto_pkcs8.h"
#include "crypto_random.h"

#include <array> // std::array
#include <cassert> // assert
#include <cstddef> // std::size_t
#include <cstdint> // std::uint8_t, std::uint32_t
#include <exception> // std::exception
#include <optional> // std::optional, std::nullopt
#include <span> // std::span
#include <string> // std::string
#include <string_view> // std::string_view
#include <utility> // std::move, std::unreachable
Expand Down Expand Up @@ -603,6 +606,48 @@ auto make_ec_private_key(const EllipticCurve curve,
.coordinate_y = expected.second}};
}

auto generate_ec_private_key(const EllipticCurve curve)
-> std::optional<PrivateKey> {
const auto parameters{to_curve_parameters(curve)};
const auto order_bits{bignum_bit_length(parameters.order)};
const auto width{curve_field_bytes(curve)};
const auto excess_bits{(width * 8u) - order_bits};

// Rejection sampling into [1, order). Masking the surplus high bits keeps the
// retry rate low even for P-521, whose order fills only 521 of its 528 bits
std::string scalar(width, '\x00');
const SecureStringScope scalar_scope{scalar};
Bignum scalar_number;

@augmentcode augmentcode Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scalar_number contains the private scalar during rejection sampling, but it isn’t wiped with secure_zero/SecureBignumScope, so secret material may linger in stack memory after return (including the early return std::nullopt path). Consider ensuring the Bignum holding secret material is securely cleared on all exit paths.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
const SecureBignumScope scalar_number_scope{scalar_number};
do {
try {
fill_random_bytes(std::span<std::uint8_t>{
reinterpret_cast<std::uint8_t *>(scalar.data()), width});
} catch (const std::exception &) {
return std::nullopt;
}

scalar[0] =
static_cast<char>(static_cast<std::uint8_t>(scalar[0]) &
static_cast<std::uint8_t>(0xffu >> excess_bits));
scalar_number = bignum_from_bytes(scalar);
} while (bignum_is_zero(scalar_number) ||
bignum_compare(scalar_number, parameters.order) >= 0);

const auto point{ec_public_from_scalar(curve, scalar)};
return PrivateKey{
new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve,
.modulus = {},
.public_exponent = {},
.private_exponent = {},
.scalar = scalar,
.elliptic_curve = curve,
.edwards_seed = {},
.edwards_curve = {},
.coordinate_x = point.first,
.coordinate_y = point.second}};
}

auto make_edwards_private_key(const EdwardsCurve curve,
const std::string_view seed)
-> std::optional<PrivateKey> {
Expand Down
29 changes: 29 additions & 0 deletions src/core/crypto/crypto_sign_windows.cc
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,35 @@ auto make_ec_private_key(const EllipticCurve curve,
.edwards_curve = {}}};
}

auto generate_ec_private_key(const EllipticCurve curve)
-> std::optional<PrivateKey> {
BCRYPT_ALG_HANDLE algorithm{nullptr};
if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(
&algorithm, to_ecdsa_algorithm(curve), nullptr, 0))) {
return std::nullopt;
}

BCRYPT_KEY_HANDLE key{nullptr};
if (!BCRYPT_SUCCESS(BCryptGenerateKeyPair(
algorithm, &key, static_cast<ULONG>(curve_bit_length(curve)), 0)) ||
!BCRYPT_SUCCESS(BCryptFinalizeKeyPair(key, 0))) {
if (key != nullptr) {
BCryptDestroyKey(key);
}

BCryptCloseAlgorithmProvider(algorithm, 0);
return std::nullopt;
}

return PrivateKey{

@augmentcode augmentcode Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If new PrivateKey::Internal{...} throws here, algorithm/key ownership hasn’t transferred yet and the handles will leak. Other locations where this applies: src/core/crypto/crypto_sign_apple.cc:387, src/core/crypto/crypto_sign_openssl.cc:354.

Severity: low

Other Locations
  • src/core/crypto/crypto_sign_apple.cc:387
  • src/core/crypto/crypto_sign_openssl.cc:354

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

new PrivateKey::Internal{.kind = PrivateKey::Type::EllipticCurve,
.algorithm = algorithm,
.key = key,
.field_bytes = curve_field_bytes(curve),
.edwards_seed = {},
.edwards_curve = {}}};
}

auto make_edwards_private_key(const EdwardsCurve curve,
const std::string_view seed)
-> std::optional<PrivateKey> {
Expand Down
15 changes: 15 additions & 0 deletions src/core/crypto/include/sourcemeta/core/crypto_sign.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,21 @@ auto SOURCEMETA_CORE_CRYPTO_EXPORT make_ec_private_key(
const std::string_view coordinate_x, const std::string_view coordinate_y)
-> std::optional<PrivateKey>;

/// @ingroup crypto
/// Generate a new random elliptic curve private key on the given curve,
/// returning no value when the platform cannot produce one. For example:
///
/// ```cpp
/// #include <sourcemeta/core/crypto.h>
/// #include <cassert>
///
/// const auto key{sourcemeta::core::generate_ec_private_key(
/// sourcemeta::core::EllipticCurve::P256)};
/// assert(key.has_value());
/// ```
auto SOURCEMETA_CORE_CRYPTO_EXPORT
generate_ec_private_key(const EllipticCurve curve) -> std::optional<PrivateKey>;

/// @ingroup crypto
/// Parse an Edwards-curve private key from its raw seed, returning no value
/// when the seed is the wrong length for the curve.
Expand Down
30 changes: 30 additions & 0 deletions test/crypto/crypto_ecdh_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,33 @@ TEST(ecdh_derive_rejects_a_mismatched_curve) {
sourcemeta::core::ecdh_derive(private_key.value(), public_key.value())
.has_value());
}

TEST(ecdh_es_round_trips_with_generated_keys) {
const auto alice{sourcemeta::core::generate_ec_private_key(
sourcemeta::core::EllipticCurve::P256)};
const auto bob{sourcemeta::core::generate_ec_private_key(
sourcemeta::core::EllipticCurve::P256)};
EXPECT_TRUE(alice.has_value());
EXPECT_TRUE(bob.has_value());
const auto alice_public{sourcemeta::core::derive_public_key(alice.value())};
const auto bob_public{sourcemeta::core::derive_public_key(bob.value())};
EXPECT_TRUE(alice_public.has_value());
EXPECT_TRUE(bob_public.has_value());

const auto alice_secret{
sourcemeta::core::ecdh_derive(alice.value(), bob_public.value())};
const auto bob_secret{
sourcemeta::core::ecdh_derive(bob.value(), alice_public.value())};
EXPECT_TRUE(alice_secret.has_value());
EXPECT_TRUE(bob_secret.has_value());
EXPECT_EQ(alice_secret.value(), bob_secret.value());

// The full ECDH-ES key derivation agrees on both sides
const auto alice_key{sourcemeta::core::kdf_concat(alice_secret.value(),
"A128GCM", "", "", 16)};
const auto bob_key{
sourcemeta::core::kdf_concat(bob_secret.value(), "A128GCM", "", "", 16)};
EXPECT_TRUE(alice_key.has_value());
EXPECT_TRUE(bob_key.has_value());
EXPECT_EQ(alice_key, bob_key);
}
56 changes: 56 additions & 0 deletions test/crypto/crypto_sign_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,24 @@ auto ecdsa_signature_verifies(
signature.value());
}

// A generated key must be internally consistent: the public key derived from
// it verifies a signature the private key produces
auto generated_key_signs_and_verifies(
const sourcemeta::core::EllipticCurve curve,
const sourcemeta::core::SignatureHashFunction hash) -> bool {
const auto key{sourcemeta::core::generate_ec_private_key(curve)};
if (!key.has_value()) {
return false;
}

const auto public_key{sourcemeta::core::derive_public_key(key.value())};
const auto signature{
sourcemeta::core::ecdsa_sign(key.value(), hash, MESSAGE)};
return public_key.has_value() && signature.has_value() &&
sourcemeta::core::ecdsa_verify(public_key.value(), hash, MESSAGE,
signature.value());
}

// Re-wrap a PKCS#8 PEM with a single extra byte appended to its DER, producing
// an otherwise valid document that is no longer canonical
auto with_trailing_der_byte(const std::string_view pem) -> std::string {
Expand Down Expand Up @@ -451,6 +469,44 @@ TEST(ecdsa_p521_sha512_signature_verifies) {
P521_D_HEX, P521_QX_HEX, P521_QY_HEX));
}

TEST(generate_ec_private_key_produces_a_functional_p256_key) {
EXPECT_TRUE(generated_key_signs_and_verifies(
sourcemeta::core::EllipticCurve::P256,
sourcemeta::core::SignatureHashFunction::SHA256));
}

TEST(generate_ec_private_key_produces_a_functional_p384_key) {
EXPECT_TRUE(generated_key_signs_and_verifies(
sourcemeta::core::EllipticCurve::P384,
sourcemeta::core::SignatureHashFunction::SHA384));
}

TEST(generate_ec_private_key_produces_a_functional_p521_key) {
EXPECT_TRUE(generated_key_signs_and_verifies(
sourcemeta::core::EllipticCurve::P521,
sourcemeta::core::SignatureHashFunction::SHA512));
}

TEST(generate_ec_private_key_is_randomized) {
const auto first{sourcemeta::core::generate_ec_private_key(
sourcemeta::core::EllipticCurve::P256)};
const auto second{sourcemeta::core::generate_ec_private_key(
sourcemeta::core::EllipticCurve::P256)};
EXPECT_TRUE(first.has_value());
EXPECT_TRUE(second.has_value());
const auto first_public{sourcemeta::core::derive_public_key(first.value())};
const auto second_public{sourcemeta::core::derive_public_key(second.value())};
EXPECT_TRUE(first_public.has_value());
EXPECT_TRUE(second_public.has_value());
const auto first_components{
sourcemeta::core::ec_public_components(first_public.value())};
const auto second_components{
sourcemeta::core::ec_public_components(second_public.value())};
EXPECT_TRUE(first_components.has_value());
EXPECT_TRUE(second_components.has_value());
EXPECT_NE(first_components.value().x, second_components.value().x);
}

TEST(ecdsa_deterministic_nonce_matches_rfc6979_known_answer) {
const auto key{sourcemeta::core::make_ec_private_key(
sourcemeta::core::EllipticCurve::P256,
Expand Down
Loading