diff --git a/src/core/crypto/crypto_helpers.h b/src/core/crypto/crypto_helpers.h index f89358fc4..96e66a940 100644 --- a/src/core/crypto/crypto_helpers.h +++ b/src/core/crypto/crypto_helpers.h @@ -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 diff --git a/src/core/crypto/crypto_sign_apple.cc b/src/core/crypto/crypto_sign_apple.cc index 877e14f06..09de7538a 100644 --- a/src/core/crypto/crypto_sign_apple.cc +++ b/src/core/crypto/crypto_sign_apple.cc @@ -357,6 +357,41 @@ auto make_ec_private_key(const EllipticCurve curve, .edwards_curve = {}}}; } +auto generate_ec_private_key(const EllipticCurve curve) + -> std::optional { + const int bits{static_cast(curve_bit_length(curve))}; + auto size{CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &bits)}; + if (size == nullptr) { + return std::nullopt; + } + + std::array attribute_keys{ + {kSecAttrKeyType, kSecAttrKeySizeInBits}}; + std::array attribute_values{ + {kSecAttrKeyTypeECSECPrimeRandom, size}}; + auto attributes{CFDictionaryCreate( + kCFAllocatorDefault, attribute_keys.data(), attribute_values.data(), + static_cast(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, + .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 { diff --git a/src/core/crypto/crypto_sign_openssl.cc b/src/core/crypto/crypto_sign_openssl.cc index 3839abdbd..1912651bb 100644 --- a/src/core/crypto/crypto_sign_openssl.cc +++ b/src/core/crypto/crypto_sign_openssl.cc @@ -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 { + 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 { diff --git a/src/core/crypto/crypto_sign_other.cc b/src/core/crypto/crypto_sign_other.cc index a804bec66..19d6cf8cc 100644 --- a/src/core/crypto/crypto_sign_other.cc +++ b/src/core/crypto/crypto_sign_other.cc @@ -8,12 +8,15 @@ #include "crypto_helpers.h" #include "crypto_other.h" #include "crypto_pkcs8.h" +#include "crypto_random.h" #include // std::array #include // assert #include // std::size_t #include // std::uint8_t, std::uint32_t +#include // std::exception #include // std::optional, std::nullopt +#include // std::span #include // std::string #include // std::string_view #include // std::move, std::unreachable @@ -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 { + 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; + const SecureBignumScope scalar_number_scope{scalar_number}; + do { + try { + fill_random_bytes(std::span{ + reinterpret_cast(scalar.data()), width}); + } catch (const std::exception &) { + return std::nullopt; + } + + scalar[0] = + static_cast(static_cast(scalar[0]) & + static_cast(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 { diff --git a/src/core/crypto/crypto_sign_windows.cc b/src/core/crypto/crypto_sign_windows.cc index fb5830514..d1b6896ee 100644 --- a/src/core/crypto/crypto_sign_windows.cc +++ b/src/core/crypto/crypto_sign_windows.cc @@ -432,6 +432,35 @@ auto make_ec_private_key(const EllipticCurve curve, .edwards_curve = {}}}; } +auto generate_ec_private_key(const EllipticCurve curve) + -> std::optional { + 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(curve_bit_length(curve)), 0)) || + !BCRYPT_SUCCESS(BCryptFinalizeKeyPair(key, 0))) { + if (key != nullptr) { + BCryptDestroyKey(key); + } + + BCryptCloseAlgorithmProvider(algorithm, 0); + return std::nullopt; + } + + return PrivateKey{ + 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 { diff --git a/src/core/crypto/include/sourcemeta/core/crypto_sign.h b/src/core/crypto/include/sourcemeta/core/crypto_sign.h index ab2786597..6f07e1d84 100644 --- a/src/core/crypto/include/sourcemeta/core/crypto_sign.h +++ b/src/core/crypto/include/sourcemeta/core/crypto_sign.h @@ -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; +/// @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 +/// #include +/// +/// 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; + /// @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. diff --git a/test/crypto/crypto_ecdh_test.cc b/test/crypto/crypto_ecdh_test.cc index 44b8d90fe..4ba05db98 100644 --- a/test/crypto/crypto_ecdh_test.cc +++ b/test/crypto/crypto_ecdh_test.cc @@ -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); +} diff --git a/test/crypto/crypto_sign_test.cc b/test/crypto/crypto_sign_test.cc index 8de2e45a6..bcef00456 100644 --- a/test/crypto/crypto_sign_test.cc +++ b/test/crypto/crypto_sign_test.cc @@ -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 { @@ -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,