current commit
261106.4 · crypto: prevent stale OpenSSL errors
#include "ncrypto.h"
#include <openssl/asn1.h>
#include <openssl/bn.h>
#include <openssl/dh.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/pem.h>
#include <openssl/pkcs12.h>
#include <openssl/rand.h>
#include <openssl/x509v3.h>
#if NCRYPTO_USE_OPENSSL_PROVIDER
#include <openssl/decoder.h>
#endif
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
#include <openssl/bytestring.h>
#include <openssl/cipher.h>
#endif
#include <algorithm>
#include <array>
#include <atomic>
#include <climits>
#include <cstring>
#include <string_view>
#include <vector>
#ifndef OPENSSL_IS_BORINGSSL
#include <openssl/core_names.h>
#include <openssl/params.h>
#include <openssl/provider.h>
#include <openssl/store.h>
#include <openssl/ui.h>
#if OPENSSL_WITH_ARGON2
#include <openssl/thread.h>
#endif
#endif

// Ubuntu 22.04's default libssl-dev still lacks EVP_PKEY_PRIVATE_KEY when
// fully updated. The macro was added upstream in OpenSSL 3.0.12.
#if NCRYPTO_USE_OPENSSL_PROVIDER && !defined(EVP_PKEY_PRIVATE_KEY)
#define EVP_PKEY_PRIVATE_KEY                                                   \
  (EVP_PKEY_KEY_PARAMETERS | OSSL_KEYMGMT_SELECT_PRIVATE_KEY)
#endif

namespace ncrypto {
namespace {
using BignumCtxPointer = DeleteFnPtr<BN_CTX, BN_CTX_free>;
using BignumGenCallbackPointer = DeleteFnPtr<BN_GENCB, BN_GENCB_free>;
using NetscapeSPKIPointer = DeleteFnPtr<NETSCAPE_SPKI, NETSCAPE_SPKI_free>;

struct ASN1SequenceDeleter final {
  void operator()(ASN1_SEQUENCE_ANY* sequence) const {
    sk_ASN1_TYPE_pop_free(sequence, ASN1_TYPE_free);
  }
};

using ASN1SequencePointer =
    std::unique_ptr<ASN1_SEQUENCE_ANY, ASN1SequenceDeleter>;

#if NCRYPTO_USE_OPENSSL_PROVIDER
using X509PubKeyPointer = DeleteFnPtr<X509_PUBKEY, X509_PUBKEY_free>;
// OSSL_STORE_close() returns int, so it needs a void-returning adapter to be
// usable as a DeleteFnPtr deleter.
void CloseStoreCtx(OSSL_STORE_CTX* ctx) {
  OSSL_STORE_close(ctx);
}
using StoreCtxPointer = DeleteFnPtr<OSSL_STORE_CTX, CloseStoreCtx>;
using UIMethodPointer = DeleteFnPtr<UI_METHOD, UI_destroy_method>;
#endif

const EVP_CIPHER* GetCipherCtxCipher(const EVP_CIPHER_CTX* ctx) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return EVP_CIPHER_CTX_get0_cipher(ctx);
#elif NCRYPTO_USE_BORINGSSL
  return EVP_CIPHER_CTX_cipher(ctx);
#else
#error "Unsupported crypto backend"
#endif
}

const EVP_MD* GetDigestCtxMd(const EVP_MD_CTX* ctx) {
  return EVP_MD_CTX_get0_md(ctx);
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
using ASN1StringPointer = DeleteFnPtr<ASN1_STRING, ASN1_STRING_free>;
using OSSLParamBldPointer = DeleteFnPtr<OSSL_PARAM_BLD, OSSL_PARAM_BLD_free>;
using RsaPssParamsPointer = DeleteFnPtr<RSA_PSS_PARAMS, RSA_PSS_PARAMS_free>;
using X509AlgorPointer = DeleteFnPtr<X509_ALGOR, X509_ALGOR_free>;
using X509PubkeyPointer = DeleteFnPtr<X509_PUBKEY, X509_PUBKEY_free>;
struct OSSLParamDeleter {
  void operator()(OSSL_PARAM* params) const {
    if (params == nullptr) return;
    for (OSSL_PARAM* param = params; param->key != nullptr; param++) {
      if (param->data != nullptr && param->data_type != OSSL_PARAM_UTF8_PTR &&
          param->data_type != OSSL_PARAM_OCTET_PTR) {
        OPENSSL_cleanse(param->data, param->data_size);
      }
    }
    OSSL_PARAM_free(params);
  }
};
using OSSLParamPointer = std::unique_ptr<OSSL_PARAM, OSSLParamDeleter>;
struct OpenSSLBufferDeleter {
  void operator()(unsigned char* pointer) const { OPENSSL_free(pointer); }
};
using OpenSSLBufferPointer =
    std::unique_ptr<unsigned char, OpenSSLBufferDeleter>;

struct RsaOtherPrimeParamNames {
  const char* factor;
  const char* exponent;
  const char* coefficient;
};

#define RSA_OTHER_PRIME_PARAM_NAMES(prime, coefficient)                        \
  {                                                                            \
    OSSL_PKEY_PARAM_RSA_FACTOR #prime, OSSL_PKEY_PARAM_RSA_EXPONENT #prime,    \
        OSSL_PKEY_PARAM_RSA_COEFFICIENT #coefficient                           \
  }

constexpr std::array<RsaOtherPrimeParamNames, 8> kRsaOtherPrimeParamNames = {{
    RSA_OTHER_PRIME_PARAM_NAMES(3, 2),
    RSA_OTHER_PRIME_PARAM_NAMES(4, 3),
    RSA_OTHER_PRIME_PARAM_NAMES(5, 4),
    RSA_OTHER_PRIME_PARAM_NAMES(6, 5),
    RSA_OTHER_PRIME_PARAM_NAMES(7, 6),
    RSA_OTHER_PRIME_PARAM_NAMES(8, 7),
    RSA_OTHER_PRIME_PARAM_NAMES(9, 8),
    RSA_OTHER_PRIME_PARAM_NAMES(10, 9),
}};

#undef RSA_OTHER_PRIME_PARAM_NAMES
#endif

static constexpr int kX509NameFlagsRFC2253WithinUtf8JSON =
    XN_FLAG_RFC2253 & ~ASN1_STRFLGS_ESC_MSB & ~ASN1_STRFLGS_ESC_CTRL;

#if NCRYPTO_USE_OPENSSL_PROVIDER
template <typename Pointer>
bool GetPKeyBnParam(const EVP_PKEY* pkey, const char* name, Pointer* out) {
  BIGNUM* bn = nullptr;
  if (pkey == nullptr) return false;
  if (EVP_PKEY_get_bn_param(pkey, name, &bn) == 1) {
    out->reset(bn);
    return true;
  }

  size_t len = 0;
  if (EVP_PKEY_get_octet_string_param(pkey, name, nullptr, 0, &len) != 1) {
    return false;
  }
  auto data = DataPointer::Alloc(len);
  if (!data ||
      EVP_PKEY_get_octet_string_param(pkey,
                                      name,
                                      static_cast<unsigned char*>(data.get()),
                                      data.size(),
                                      &len) != 1) {
    return false;
  }
  bn = BN_bin2bn(static_cast<const unsigned char*>(data.get()), len, nullptr);
  if (bn == nullptr) return false;
  out->reset(bn);
  return true;
}

template <typename Pointer>
bool GetOptionalPKeyBnParam(const EVP_PKEY* pkey,
                            const char* name,
                            Pointer* out) {
  BIGNUM* bn = nullptr;
  if (pkey == nullptr) {
    out->reset();
    return true;
  }
  if (EVP_PKEY_get_bn_param(pkey, name, &bn) == 1) {
    out->reset(bn);
    return true;
  }

  size_t len = 0;
  if (EVP_PKEY_get_octet_string_param(pkey, name, nullptr, 0, &len) == 1) {
    auto data = DataPointer::Alloc(len);
    if (!data ||
        EVP_PKEY_get_octet_string_param(pkey,
                                        name,
                                        static_cast<unsigned char*>(data.get()),
                                        data.size(),
                                        &len) != 1) {
      return false;
    }
    bn = BN_bin2bn(static_cast<const unsigned char*>(data.get()), len, nullptr);
    if (bn == nullptr) return false;
    out->reset(bn);
    return true;
  }

  out->reset();
  return true;
}

EVPKeyPointer NewPKeyFromData(const KeyAlgorithm& algorithm,
                              int selection,
                              OSSL_PARAM* params) {
  auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(algorithm);
  if (!ctx || EVP_PKEY_fromdata_init(ctx.get()) != 1) return {};

  EVP_PKEY* pkey = nullptr;
  if (EVP_PKEY_fromdata(ctx.get(), &pkey, selection, params) != 1) {
    return {};
  }
  return EVPKeyPointer(pkey);
}

EVPKeyPointer NewDhPKey(const BIGNUM* p,
                        const BIGNUM* g,
                        const BIGNUM* pub = nullptr,
                        const BIGNUM* priv = nullptr) {
  if (p == nullptr || g == nullptr) return {};

  OSSLParamBldPointer bld(OSSL_PARAM_BLD_new());
  if (!bld ||
      OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_FFC_P, p) != 1 ||
      OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_FFC_G, g) != 1) {
    return {};
  }

  int selection = EVP_PKEY_KEY_PARAMETERS;
  if (pub != nullptr) {
    if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_PUB_KEY, pub) != 1) {
      return {};
    }
    selection |= EVP_PKEY_PUBLIC_KEY;
  }
  if (priv != nullptr) {
    if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_PRIV_KEY, priv) !=
        1) {
      return {};
    }
    selection |= EVP_PKEY_PRIVATE_KEY;
  }

  OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get()));
  if (!params) return {};
  return NewPKeyFromData(KeyAlgorithm::DH, selection, params.get());
}

EVPKeyPointer NewDhPKey(const char* group_name,
                        const BIGNUM* pub = nullptr,
                        const BIGNUM* priv = nullptr) {
  if (group_name == nullptr) return {};

  if (pub == nullptr && priv == nullptr) {
    auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::DH);
    OSSL_PARAM params[] = {
        OSSL_PARAM_construct_utf8_string(
            OSSL_PKEY_PARAM_GROUP_NAME, const_cast<char*>(group_name), 0),
        OSSL_PARAM_END,
    };
    if (!ctx || !ctx.initForParamgen() ||
        EVP_PKEY_CTX_set_params(ctx.get(), params) != 1) {
      return {};
    }
    return ctx.paramgen();
  }

  OSSLParamBldPointer bld(OSSL_PARAM_BLD_new());
  if (!bld || OSSL_PARAM_BLD_push_utf8_string(
                  bld.get(), OSSL_PKEY_PARAM_GROUP_NAME, group_name, 0) != 1) {
    return {};
  }

  int selection = EVP_PKEY_KEY_PARAMETERS;
  if (pub != nullptr) {
    if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_PUB_KEY, pub) != 1) {
      return {};
    }
    selection |= EVP_PKEY_PUBLIC_KEY;
  }
  if (priv != nullptr) {
    if (OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_PRIV_KEY, priv) !=
        1) {
      return {};
    }
    selection |= EVP_PKEY_PRIVATE_KEY;
  }

  OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get()));
  if (!params) return {};
  return NewPKeyFromData(KeyAlgorithm::DH, selection, params.get());
}

bool GetDhParams(const EVP_PKEY* pkey,
                 DeleteFnPtr<BIGNUM, BN_free>* p,
                 DeleteFnPtr<BIGNUM, BN_free>* g,
                 DeleteFnPtr<BIGNUM, BN_free>* q = nullptr,
                 DeleteFnPtr<BIGNUM, BN_free>* j = nullptr) {
  return GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_P, p) &&
         GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_G, g) &&
         (q == nullptr ||
          GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_Q, q)) &&
         (j == nullptr ||
          GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_COFACTOR, j));
}

bool GetDhKeys(const EVP_PKEY* pkey,
               DeleteFnPtr<BIGNUM, BN_free>* pub,
               DeleteFnPtr<BIGNUM, BN_clear_free>* priv) {
  return (pub == nullptr ||
          GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_PUB_KEY, pub)) &&
         (priv == nullptr ||
          GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_PRIV_KEY, priv));
}
#endif

#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
struct BoringSSLCipher {
  const EVP_CIPHER* (*get)();
  const char* name;
};

constexpr BoringSSLCipher kBoringSSLCiphers[] = {
    {EVP_aes_128_cbc, "aes-128-cbc"},   {EVP_aes_128_ctr, "aes-128-ctr"},
    {EVP_aes_128_ecb, "aes-128-ecb"},   {EVP_aes_128_gcm, "aes-128-gcm"},
    {EVP_aes_128_ofb, "aes-128-ofb"},   {EVP_aes_192_cbc, "aes-192-cbc"},
    {EVP_aes_192_ctr, "aes-192-ctr"},   {EVP_aes_192_ecb, "aes-192-ecb"},
    {EVP_aes_192_gcm, "aes-192-gcm"},   {EVP_aes_192_ofb, "aes-192-ofb"},
    {EVP_aes_256_cbc, "aes-256-cbc"},   {EVP_aes_256_ctr, "aes-256-ctr"},
    {EVP_aes_256_ecb, "aes-256-ecb"},   {EVP_aes_256_gcm, "aes-256-gcm"},
    {EVP_aes_256_ofb, "aes-256-ofb"},   {EVP_des_cbc, "des-cbc"},
    {EVP_des_ecb, "des-ecb"},           {EVP_des_ede, "des-ede"},
    {EVP_des_ede3_cbc, "des-ede3-cbc"}, {EVP_des_ede_cbc, "des-ede-cbc"},
    {EVP_rc2_cbc, "rc2-cbc"},           {EVP_rc4, "rc4"},
};

#endif
}  // namespace

// ============================================================================

namespace {
// ERR_clear_error() resets every slot of the error queue even when it is
// empty, which costs several times more than checking for an error first.
void ClearErrorQueue() {
  if (ERR_peek_error() != 0) ERR_clear_error();
}
}  // namespace

ClearErrorOnReturn::ClearErrorOnReturn(CryptoErrorList* errors)
    : errors_(errors) {
  ClearErrorQueue();
}

ClearErrorOnReturn::~ClearErrorOnReturn() {
  if (errors_ != nullptr) errors_->capture();
  ClearErrorQueue();
}

unsigned long ClearErrorOnReturn::peekError() {  // NOLINT(runtime/int)
  return ERR_peek_error();
}

MarkPopErrorOnReturn::MarkPopErrorOnReturn(CryptoErrorList* errors)
    : errors_(errors) {
  ERR_set_mark();
}

MarkPopErrorOnReturn::~MarkPopErrorOnReturn() {
  if (errors_ != nullptr) errors_->capture();
  ERR_pop_to_mark();
}

unsigned long MarkPopErrorOnReturn::peekError() {  // NOLINT(runtime/int)
  return ERR_peek_error();
}

CryptoErrorList::CryptoErrorList(CryptoErrorList::Option option) {
  if (option == Option::CAPTURE_ON_CONSTRUCT) capture();
}

void CryptoErrorList::capture() {
  errors_.clear();
  while (const auto err = ERR_get_error()) {
    char buf[256];
    ERR_error_string_n(err, buf, sizeof(buf));
    errors_.emplace_front(buf);
  }
}

void CryptoErrorList::add(std::string error) {
  errors_.push_back(error);
}

std::optional<std::string> CryptoErrorList::pop_back() {
  if (errors_.empty()) return std::nullopt;
  std::string error = errors_.back();
  errors_.pop_back();
  return error;
}

std::optional<std::string> CryptoErrorList::pop_front() {
  if (errors_.empty()) return std::nullopt;
  std::string error = errors_.front();
  errors_.pop_front();
  return error;
}

// ============================================================================
DataPointer DataPointer::Alloc(size_t len) {
  return DataPointer(OPENSSL_zalloc(len), len);
}

DataPointer DataPointer::SecureAlloc(size_t len) {
#ifndef OPENSSL_IS_BORINGSSL
  auto ptr = OPENSSL_secure_zalloc(len);
  if (ptr == nullptr) return {};
  // OPENSSL_secure_zalloc transparently falls back to a regular allocation
  // when the secure heap is not initialized or is exhausted. Reflect the
  // actual provenance of the pointer so that reset() routes to the correct
  // free function (OPENSSL_secure_clear_free vs. OPENSSL_clear_free) and
  // callers of isSecure() get a truthful answer.
  return DataPointer(ptr, len, CRYPTO_secure_allocated(ptr) == 1);
#elif defined(OPENSSL_IS_BORINGSSL)
  // BoringSSL does not implement the OPENSSL_secure_zalloc API.
  auto ptr = OPENSSL_malloc(len);
  if (ptr == nullptr) return {};
  memset(ptr, 0, len);
  return DataPointer(ptr, len);
#endif
}

size_t DataPointer::GetSecureHeapUsed() {
  return CRYPTO_secure_malloc_initialized() ? CRYPTO_secure_used() : 0;
}

DataPointer::InitSecureHeapResult DataPointer::TryInitSecureHeap(size_t amount,
                                                                 size_t min) {
  switch (CRYPTO_secure_malloc_init(amount, min)) {
    case 0:
      return InitSecureHeapResult::FAILED;
    case 2:
      return InitSecureHeapResult::UNABLE_TO_MEMORY_MAP;
    case 1:
      return InitSecureHeapResult::OK;
    default:
      return InitSecureHeapResult::FAILED;
  }
}

DataPointer DataPointer::Copy(const Buffer<const void>& buffer) {
  return DataPointer(OPENSSL_memdup(buffer.data, buffer.len), buffer.len);
}

DataPointer::DataPointer(void* data, size_t length, bool secure)
    : data_(data), len_(length), secure_(secure) {}

DataPointer::DataPointer(const Buffer<void>& buffer, bool secure)
    : data_(buffer.data), len_(buffer.len), secure_(secure) {}

DataPointer::DataPointer(DataPointer&& other) noexcept
    : data_(other.data_), len_(other.len_), secure_(other.secure_) {
  other.data_ = nullptr;
  other.len_ = 0;
  other.secure_ = false;
}

DataPointer& DataPointer::operator=(DataPointer&& other) noexcept {
  if (this == &other) return *this;
  this->~DataPointer();
  return *new (this) DataPointer(std::move(other));
}

DataPointer::~DataPointer() {
  reset();
}

void DataPointer::zero() {
  if (!data_) return;
  OPENSSL_cleanse(data_, len_);
}

void DataPointer::reset(void* data, size_t length) {
  if (data_ != nullptr) {
    if (secure_) {
      OPENSSL_secure_clear_free(data_, len_);
    } else {
      OPENSSL_clear_free(data_, len_);
    }
  }
  data_ = data;
  len_ = length;
}

void DataPointer::reset(const Buffer<void>& buffer) {
  reset(buffer.data, buffer.len);
}

Buffer<void> DataPointer::release() {
  Buffer<void> buf{
      .data = data_,
      .len = len_,
  };
  data_ = nullptr;
  len_ = 0;
  return buf;
}

DataPointer DataPointer::resize(size_t len) {
  const size_t actual_len = std::min(len_, len);
  if (actual_len == len_) return std::move(*this);

  auto buf = release();
  if (actual_len == 0) return DataPointer(buf.data, actual_len);

  buf.data = OPENSSL_realloc(buf.data, actual_len);
  buf.len = actual_len;
  return DataPointer(buf);
}

// ============================================================================
namespace {
// This generation only coordinates cache invalidation. It does not make
// OpenSSL default property changes safe to race with crypto operations.
std::atomic<uint64_t> fips_state_generation{0};

bool isFipsEnabledRaw() {
#ifndef OPENSSL_IS_BORINGSSL
  return EVP_default_properties_is_fips_enabled(nullptr) == 1;
#elif defined(OPENSSL_IS_BORINGSSL)
  return FIPS_mode() == 1;
#endif
}
}  // namespace

bool isFipsEnabled() {
  ClearErrorOnReturn clear_error_on_return;
  return isFipsEnabledRaw();
}

bool setFipsEnabled(bool enable, CryptoErrorList* errors) {
  const bool was_enabled = isFipsEnabled();
  if (was_enabled == enable) return true;
  ClearErrorOnReturn clearErrorOnReturn(errors);
#ifndef OPENSSL_IS_BORINGSSL
  const bool success =
      EVP_default_properties_enable_fips(nullptr, enable ? 1 : 0) == 1;
#elif defined(OPENSSL_IS_BORINGSSL)
  const bool success = FIPS_mode_set(enable ? 1 : 0) == 1;
#endif
  if (success && isFipsEnabledRaw() != was_enabled) {
    fips_state_generation.fetch_add(1, std::memory_order_release);
  }
  return success;
}

uint64_t getFipsStateGeneration() {
  return fips_state_generation.load(std::memory_order_acquire);
}

bool testFipsEnabled() {
  ClearErrorOnReturn clear_error_on_return;
#ifndef OPENSSL_IS_BORINGSSL
  OSSL_PROVIDER* fips_provider = nullptr;
  if (OSSL_PROVIDER_available(nullptr, "fips")) {
    fips_provider = OSSL_PROVIDER_load(nullptr, "fips");
  }
  if (fips_provider == nullptr) return false;
  int result = OSSL_PROVIDER_self_test(fips_provider);
  OSSL_PROVIDER_unload(fips_provider);
  return result;
#elif defined(OPENSSL_IS_BORINGSSL)
#ifdef OPENSSL_FIPS
  return FIPS_selftest();
#else  // OPENSSL_FIPS
  return false;
#endif  // OPENSSL_FIPS
#endif
}

// ============================================================================
// Bignum
BignumPointer::BignumPointer(BIGNUM* bignum) : bn_(bignum) {}

BignumPointer::BignumPointer(const unsigned char* data, size_t len)
    : BignumPointer(BN_bin2bn(data, len, nullptr)) {}

BignumPointer::BignumPointer(BignumPointer&& other) noexcept
    : bn_(other.release()) {}

BignumPointer BignumPointer::New() {
  return BignumPointer(BN_new());
}

BignumPointer BignumPointer::NewSecure() {
  return BignumPointer(BN_secure_new());
}

BignumPointer& BignumPointer::operator=(BignumPointer&& other) noexcept {
  if (this == &other) return *this;
  this->~BignumPointer();
  return *new (this) BignumPointer(std::move(other));
}

BignumPointer::~BignumPointer() {
  reset();
}

void BignumPointer::reset(BIGNUM* bn) {
  bn_.reset(bn);
}

void BignumPointer::reset(const unsigned char* data, size_t len) {
  reset(BN_bin2bn(data, len, nullptr));
}

BIGNUM* BignumPointer::release() {
  return bn_.release();
}

size_t BignumPointer::byteLength() const {
  if (bn_ == nullptr) return 0;
  return BN_num_bytes(bn_.get());
}

DataPointer BignumPointer::encode() const {
  return EncodePadded(bn_.get(), byteLength());
}

DataPointer BignumPointer::encodePadded(size_t size) const {
  return EncodePadded(bn_.get(), size);
}

size_t BignumPointer::encodeInto(unsigned char* out) const {
  if (!bn_) return 0;
  return BN_bn2bin(bn_.get(), out);
}

size_t BignumPointer::encodePaddedInto(unsigned char* out, size_t size) const {
  if (!bn_) return 0;
  return BN_bn2binpad(bn_.get(), out, size);
}

DataPointer BignumPointer::Encode(const BIGNUM* bn) {
  return EncodePadded(bn, bn != nullptr ? BN_num_bytes(bn) : 0);
}

bool BignumPointer::setWord(unsigned long w) {  // NOLINT(runtime/int)
  if (!bn_) return false;
  return BN_set_word(bn_.get(), w) == 1;
}

std::optional<unsigned long> BignumPointer::GetWord(  // NOLINT(runtime/int)
    const BIGNUM* bn) {
  BN_ULONG ret = BN_get_word(bn);
  if (ret == static_cast<BN_ULONG>(-1)) return std::nullopt;
  return ret;
}

std::optional<unsigned long> BignumPointer::getWord()  // NOLINT(runtime/int)
    const {
  if (!bn_) return std::nullopt;
  return GetWord(bn_.get());
}

DataPointer BignumPointer::EncodePadded(const BIGNUM* bn, size_t s) {
  if (bn == nullptr) return DataPointer();
  size_t size = std::max(s, static_cast<size_t>(GetByteCount(bn)));
  auto buf = DataPointer::Alloc(size);
  BN_bn2binpad(bn, reinterpret_cast<unsigned char*>(buf.get()), size);
  return buf;
}
size_t BignumPointer::EncodePaddedInto(const BIGNUM* bn,
                                       unsigned char* out,
                                       size_t size) {
  if (bn == nullptr) return 0;
  return BN_bn2binpad(bn, out, size);
}

int BignumPointer::operator<=>(const BignumPointer& other) const noexcept {
  if (bn_ == nullptr && other.bn_ != nullptr) return -1;
  if (bn_ != nullptr && other.bn_ == nullptr) return 1;
  if (bn_ == nullptr && other.bn_ == nullptr) return 0;
  return BN_cmp(bn_.get(), other.bn_.get());
}

int BignumPointer::operator<=>(const BIGNUM* other) const noexcept {
  if (bn_ == nullptr && other != nullptr) return -1;
  if (bn_ != nullptr && other == nullptr) return 1;
  if (bn_ == nullptr && other == nullptr) return 0;
  return BN_cmp(bn_.get(), other);
}

DataPointer BignumPointer::toHex() const {
  if (!bn_) return {};
  char* hex = BN_bn2hex(bn_.get());
  if (!hex) return {};
  return DataPointer(hex, strlen(hex));
}

int BignumPointer::GetBitCount(const BIGNUM* bn) {
  return BN_num_bits(bn);
}

int BignumPointer::GetByteCount(const BIGNUM* bn) {
  return BN_num_bytes(bn);
}

bool BignumPointer::isZero() const {
  return bn_ && BN_is_zero(bn_.get());
}

bool BignumPointer::isOne() const {
  return bn_ && BN_is_one(bn_.get());
}

const BIGNUM* BignumPointer::One() {
  return BN_value_one();
}

BignumPointer BignumPointer::clone() {
  if (!bn_) return {};
  return BignumPointer(BN_dup(bn_.get()));
}

int BignumPointer::isPrime(int nchecks,
                           BignumPointer::PrimeCheckCallback innerCb) const {
  BignumCtxPointer ctx(BN_CTX_new());
  BignumGenCallbackPointer cb(nullptr);
  if (innerCb != nullptr) {
    cb = BignumGenCallbackPointer(BN_GENCB_new());
    if (!cb) [[unlikely]]
      return -1;
    BN_GENCB_set(
        cb.get(),
        // TODO(@jasnell): This could be refactored to allow inlining.
        // Not too important right now tho.
        [](int a, int b, BN_GENCB* ctx) mutable -> int {
          PrimeCheckCallback& ptr =
              *static_cast<PrimeCheckCallback*>(BN_GENCB_get_arg(ctx));
          return ptr(a, b) ? 1 : 0;
        },
        &innerCb);
  }
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return BN_check_prime(get(), ctx.get(), cb.get());
#elif NCRYPTO_USE_BORINGSSL
  int is_probably_prime = 0;
  if (BN_primality_test(
          &is_probably_prime, get(), nchecks, ctx.get(), 0, cb.get()) != 1) {
    return -1;
  }
  return is_probably_prime;
#else
#error "Unsupported crypto backend"
#endif
}

BignumPointer BignumPointer::NewPrime(const PrimeConfig& params,
                                      PrimeCheckCallback cb) {
  BignumPointer prime(BN_new());
  if (!prime || !prime.generate(params, std::move(cb))) {
    return {};
  }
  return prime;
}

bool BignumPointer::generate(const PrimeConfig& params,
                             PrimeCheckCallback innerCb) const {
  // BN_generate_prime_ex() calls RAND_bytes_ex() internally.
  // Make sure the CSPRNG is properly seeded.
  std::ignore = CSPRNG(nullptr, 0);
  BignumGenCallbackPointer cb(nullptr);
  if (innerCb != nullptr) {
    cb = BignumGenCallbackPointer(BN_GENCB_new());
    if (!cb) [[unlikely]]
      return -1;
    BN_GENCB_set(
        cb.get(),
        [](int a, int b, BN_GENCB* ctx) mutable -> int {
          PrimeCheckCallback& ptr =
              *static_cast<PrimeCheckCallback*>(BN_GENCB_get_arg(ctx));
          return ptr(a, b) ? 1 : 0;
        },
        &innerCb);
  }
  if (BN_generate_prime_ex(get(),
                           params.bits,
                           params.safe ? 1 : 0,
                           params.add.get(),
                           params.rem.get(),
                           cb.get()) == 0) {
    return false;
  }

  return true;
}

BignumPointer BignumPointer::NewSub(const BignumPointer& a,
                                    const BignumPointer& b) {
  BignumPointer res = New();
  if (!res) return {};
  if (!BN_sub(res.get(), a.get(), b.get())) {
    return {};
  }
  return res;
}

BignumPointer BignumPointer::NewLShift(size_t length) {
  BignumPointer res = New();
  if (!res) return {};
  if (!BN_lshift(res.get(), One(), length)) {
    return {};
  }
  return res;
}

// ============================================================================
// Utility methods

bool CSPRNG(void* buffer, size_t length) {
  auto buf = reinterpret_cast<unsigned char*>(buffer);
  do {
    if (1 == RAND_status()) {
#ifndef OPENSSL_IS_BORINGSSL
      if (1 == RAND_bytes_ex(nullptr, buf, length, 0)) {
        return true;
      }
#elif defined(OPENSSL_IS_BORINGSSL)
      while (length > INT_MAX && 1 == RAND_bytes(buf, INT_MAX)) {
        buf += INT_MAX;
        length -= INT_MAX;
      }
      if (length <= INT_MAX && 1 == RAND_bytes(buf, static_cast<int>(length)))
        return true;
#endif
    }
#ifndef OPENSSL_IS_BORINGSSL
    const auto code = ERR_peek_last_error();
    // A misconfigured OpenSSL installation may report 1 from RAND_poll()
    // and RAND_status() but fail in RAND_bytes() if it cannot look up
    // a matching algorithm for the CSPRNG.
    if (ERR_GET_LIB(code) == ERR_LIB_RAND) {
      const auto reason = ERR_GET_REASON(code);
      if (reason == RAND_R_ERROR_INSTANTIATING_DRBG ||
          reason == RAND_R_UNABLE_TO_FETCH_DRBG ||
          reason == RAND_R_UNABLE_TO_CREATE_DRBG) {
        return false;
      }
    }
#endif
  } while (1 == RAND_poll());

  return false;
}

int NoPasswordCallback(char* buf, int size, int rwflag, void* u) {
  return 0;
}

int PasswordCallback(char* buf, int size, int rwflag, void* u) {
  auto passphrase = static_cast<const Buffer<char>*>(u);
  if (size <= 0) return -1;
  if (passphrase != nullptr) {
    size_t buflen = static_cast<size_t>(size);
    size_t len = passphrase->len;
    if (buflen < len) return -1;
    memcpy(buf, reinterpret_cast<const char*>(passphrase->data), len);
    return len;
  }

  return -1;
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
namespace {
struct StorePassphraseData {
  Buffer<char> passphrase{.data = nullptr, .len = 0};
  bool has_passphrase = false;
  bool missing_passphrase = false;
};

int StorePasswordCallback(char* buf, int size, int rwflag, void* u) {
  auto data = static_cast<StorePassphraseData*>(u);
  if (data == nullptr || !data->has_passphrase) {
    if (data != nullptr) data->missing_passphrase = true;
    return -1;
  }

  if (size <= 0) return -1;
  size_t buflen = static_cast<size_t>(size);
  size_t len = data->passphrase.len;
  if (buflen < len) return -1;
  memcpy(buf, reinterpret_cast<const char*>(data->passphrase.data), len);
  return len;
}
}  // namespace
#endif

// Algorithm: http://howardhinnant.github.io/date_algorithms.html
constexpr int days_from_epoch(int y, unsigned m, unsigned d) {
  y -= m <= 2;
  const int era = (y >= 0 ? y : y - 399) / 400;
  const unsigned yoe = static_cast<unsigned>(y - era * 400);  // [0, 399]
  const unsigned doy =
      (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;          // [0, 365]
  const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;  // [0, 146096]
  return era * 146097 + static_cast<int>(doe) - 719468;
}

#ifndef OPENSSL_IS_BORINGSSL
// tm must be in UTC
// using time_t causes problems on 32-bit systems and windows x64.
int64_t PortableTimeGM(struct tm* t) {
  int year = t->tm_year + 1900;
  int month = t->tm_mon;
  if (month > 11) {
    year += month / 12;
    month %= 12;
  } else if (month < 0) {
    int years_diff = (11 - month) / 12;
    year -= years_diff;
    month += 12 * years_diff;
  }
  int days_since_epoch = days_from_epoch(year, month + 1, t->tm_mday);

  return 60 * (60 * (24LL * static_cast<int64_t>(days_since_epoch) +
                     t->tm_hour) +
               t->tm_min) +
         t->tm_sec;
}
#endif

// ============================================================================
// SPKAC

bool VerifySpkac(const char* input, size_t length) {
  ClearErrorOnReturn clear_error_on_return;
#ifdef OPENSSL_IS_BORINGSSL
  // OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters,
  // while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not.
  // As such, we trim those characters here for compatibility.
  //
  // find_last_not_of can return npos, which is the maximum value of size_t.
  // The + 1 will force a roll-ver to 0, which is the correct value. in that
  // case.
  length = std::string_view(input, length).find_last_not_of(" \n\r\t") + 1;
#endif
  NetscapeSPKIPointer spki(NETSCAPE_SPKI_b64_decode(input, length));
  if (!spki) return false;

  EVPKeyPointer pkey(X509_PUBKEY_get(spki->spkac->pubkey));
  return pkey ? NETSCAPE_SPKI_verify(spki.get(), pkey.get()) > 0 : false;
}

BIOPointer ExportPublicKey(const char* input, size_t length) {
  ClearErrorOnReturn clear_error_on_return;
  BIOPointer bio(BIO_new(BIO_s_mem()));
  if (!bio) return {};

#ifdef OPENSSL_IS_BORINGSSL
  // OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters,
  // while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not.
  // As such, we trim those characters here for compatibility.
  length = std::string_view(input, length).find_last_not_of(" \n\r\t") + 1;
#endif
  NetscapeSPKIPointer spki(NETSCAPE_SPKI_b64_decode(input, length));
  if (!spki) return {};

  EVPKeyPointer pkey(NETSCAPE_SPKI_get_pubkey(spki.get()));
  if (!pkey) return {};

  if (PEM_write_bio_PUBKEY(bio.get(), pkey.get()) <= 0) return {};

  return bio;
}

Buffer<char> ExportChallenge(const char* input, size_t length) {
  ClearErrorOnReturn clear_error_on_return;
#ifdef OPENSSL_IS_BORINGSSL
  // OpenSSL uses EVP_DecodeBlock, which explicitly removes trailing characters,
  // while BoringSSL uses EVP_DecodedLength and EVP_DecodeBase64, which do not.
  // As such, we trim those characters here for compatibility.
  length = std::string_view(input, length).find_last_not_of(" \n\r\t") + 1;
#endif
  NetscapeSPKIPointer sp(NETSCAPE_SPKI_b64_decode(input, length));
  if (!sp) return {};

  unsigned char* buf = nullptr;
  int buf_size = ASN1_STRING_to_UTF8(&buf, sp->spkac->challenge);
  if (buf_size >= 0) {
    return {
        .data = reinterpret_cast<char*>(buf),
        .len = static_cast<size_t>(buf_size),
    };
  }

  return {};
}

// ============================================================================
namespace {
enum class AltNameOption {
  NONE,
  UTF8,
};

std::optional<std::string> ObjectToText(const ASN1_OBJECT* object,
                                        bool no_name) {
  if (object == nullptr) return std::nullopt;
  int length = OBJ_obj2txt(nullptr, 0, object, no_name);
  if (length <= 0 || length == INT_MAX) return std::nullopt;

  std::string text(static_cast<size_t>(length) + 1, '\0');
  int actual = OBJ_obj2txt(text.data(), length + 1, object, no_name);
  if (actual != length) return std::nullopt;
  text.resize(static_cast<size_t>(length));
  return text;
}

bool IsSafeAltName(const char* name, size_t length, AltNameOption option) {
  for (size_t i = 0; i < length; i++) {
    char c = name[i];
    switch (c) {
      case '"':
      case '\\':
        // These mess with encoding rules.
        // Fall through.
      case ',':
        // Commas make it impossible to split the list of subject alternative
        // names unambiguously, which is why we have to escape.
        // Fall through.
      case '\'':
        // Single quotes are unlikely to appear in any legitimate values, but
        // they could be used to make a value look like it was escaped (i.e.,
        // enclosed in single/double quotes).
        return false;
      default:
        if (option == AltNameOption::UTF8) {
          // In UTF8 strings, we require escaping for any ASCII control
          // character, but NOT for non-ASCII characters. Note that all bytes of
          // any code point that consists of more than a single byte have their
          // MSB set.
          if (static_cast<unsigned char>(c) < ' ' || c == '\x7f') {
            return false;
          }
        } else {
          // Check if the char is a control character or non-ASCII character.
          // Note that char may or may not be a signed type. Regardless,
          // non-ASCII values will always be outside of this range.
          if (c < ' ' || c > '~') {
            return false;
          }
        }
    }
  }
  return true;
}

[[nodiscard]] bool PrintAltName(const BIOPointer& out,
                                const char* name,
                                size_t length,
                                AltNameOption option = AltNameOption::NONE,
                                const char* safe_prefix = nullptr) {
  if (!out || (name == nullptr && length != 0)) return false;
  if (IsSafeAltName(name, length, option)) {
    // For backward-compatibility, append "safe" names without any
    // modifications.
    if (safe_prefix != nullptr) {
      if (!BIOPointer::PrintfAll(out, "%s:", safe_prefix)) return false;
    }
    if (length != 0 &&
        !BIOPointer::WriteAll(out, std::string_view(name, length))) {
      return false;
    }
  } else {
    // If a name is not "safe", we cannot embed it without special
    // encoding. This does not usually happen, but we don't want to hide
    // it from the user either. We use JSON compatible escaping here.
    if (!BIOPointer::WriteAll(out, "\"")) return false;
    if (safe_prefix != nullptr) {
      if (!BIOPointer::PrintfAll(out, "%s:", safe_prefix)) return false;
    }
    for (size_t j = 0; j < length; j++) {
      char c = static_cast<char>(name[j]);
      if (c == '\\') {
        if (!BIOPointer::WriteAll(out, "\\\\")) return false;
      } else if (c == '"') {
        if (!BIOPointer::WriteAll(out, "\\\"")) return false;
      } else if ((c >= ' ' && c != ',' && c <= '~') ||
                 (option == AltNameOption::UTF8 && (c & 0x80))) {
        // Note that the above condition explicitly excludes commas, which means
        // that those are encoded as Unicode escape sequences in the "else"
        // block. That is not strictly necessary, and Node.js itself would parse
        // it correctly either way. We only do this to account for third-party
        // code that might be splitting the string at commas (as Node.js itself
        // used to do).
        if (!BIOPointer::WriteAll(out, std::string_view(&c, 1))) return false;
      } else {
        // Control character or non-ASCII character. We treat everything as
        // Latin-1, which corresponds to the first 255 Unicode code points.
        const char hex[] = "0123456789abcdef";
        char u[] = {'\\', 'u', '0', '0', hex[(c & 0xf0) >> 4], hex[c & 0x0f]};
        if (!BIOPointer::WriteAll(out, std::string_view(u, sizeof(u)))) {
          return false;
        }
      }
    }
    if (!BIOPointer::WriteAll(out, "\"")) return false;
  }
  return true;
}

// This function emulates the behavior of i2v_GENERAL_NAME in a safer and less
// ambiguous way. "othername:" entries use the GENERAL_NAME_print format.
bool PrintGeneralName(const BIOPointer& out, const GENERAL_NAME* gen) {
  if (!out || gen == nullptr) return false;
  if (gen->type == GEN_DNS) {
    ASN1_IA5STRING* name = gen->d.dNSName;
    if (name == nullptr) return false;
    if (!BIOPointer::WriteAll(out, "DNS:")) return false;
    // Note that the preferred name syntax (see RFCs 5280 and 1034) with
    // wildcards is a subset of what we consider "safe", so spec-compliant DNS
    // names will never need to be escaped.
    int length = ASN1_STRING_length(name);
    return length >= 0 && PrintAltName(out,
                                       reinterpret_cast<const char*>(
                                           ASN1_STRING_get0_data(name)),
                                       static_cast<size_t>(length));
  } else if (gen->type == GEN_EMAIL) {
    ASN1_IA5STRING* name = gen->d.rfc822Name;
    if (name == nullptr) return false;
    if (!BIOPointer::WriteAll(out, "email:")) return false;
    int length = ASN1_STRING_length(name);
    return length >= 0 && PrintAltName(out,
                                       reinterpret_cast<const char*>(
                                           ASN1_STRING_get0_data(name)),
                                       static_cast<size_t>(length));
  } else if (gen->type == GEN_URI) {
    ASN1_IA5STRING* name = gen->d.uniformResourceIdentifier;
    if (name == nullptr) return false;
    if (!BIOPointer::WriteAll(out, "URI:")) return false;
    // The set of "safe" names was designed to include just about any URI,
    // with a few exceptions, most notably URIs that contains commas (see
    // RFC 2396). In other words, most legitimate URIs will not require
    // escaping.
    int length = ASN1_STRING_length(name);
    return length >= 0 && PrintAltName(out,
                                       reinterpret_cast<const char*>(
                                           ASN1_STRING_get0_data(name)),
                                       static_cast<size_t>(length));
  } else if (gen->type == GEN_DIRNAME) {
    if (gen->d.dirn == nullptr) return false;
    // Earlier versions of Node.js used X509_NAME_oneline to print the X509_NAME
    // object. The format was non standard and should be avoided. The use of
    // X509_NAME_oneline is discouraged by OpenSSL but was required for backward
    // compatibility. Conveniently, X509_NAME_oneline produced ASCII and the
    // output was unlikely to contains commas or other characters that would
    // require escaping. However, it SHOULD NOT produce ASCII output since an
    // RFC5280 AttributeValue may be a UTF8String.
    // Newer versions of Node.js have since switched to X509_NAME_print_ex to
    // produce a better format at the cost of backward compatibility. The new
    // format may contain Unicode characters and it is likely to contain commas,
    // which require escaping. Fortunately, the recently safeguarded function
    // PrintAltName handles all of that safely.
    if (!BIOPointer::WriteAll(out, "DirName:")) return false;
    BIOPointer tmp = BIOPointer::NewMem();
    if (!tmp) return false;
    if (X509_NAME_print_ex(
            tmp.get(), gen->d.dirn, 0, kX509NameFlagsRFC2253WithinUtf8JSON) <
        0) {
      return false;
    }
    char* oline = nullptr;
    long n_bytes = BIO_get_mem_data(tmp.get(), &oline);  // NOLINT(runtime/int)
    if (n_bytes < 0) return false;
    return PrintAltName(out,
                        oline,
                        static_cast<size_t>(n_bytes),
                        ncrypto::AltNameOption::UTF8,
                        nullptr);
  } else if (gen->type == GEN_IPADD) {
    if (!BIOPointer::WriteAll(out, "IP Address:")) return false;
    const ASN1_OCTET_STRING* ip = gen->d.ip;
    if (ip == nullptr) return false;
    const unsigned char* b = ASN1_STRING_get0_data(ip);
    int ip_len = ASN1_STRING_length(ip);
    if (ip_len < 0 || (b == nullptr && ip_len != 0)) return false;
    if (ip_len == 4) {
      return BIOPointer::PrintfAll(out, "%d.%d.%d.%d", b[0], b[1], b[2], b[3]);
    } else if (ip_len == 16) {
      for (unsigned int j = 0; j < 8; j++) {
        uint16_t pair = (b[2 * j] << 8) | b[2 * j + 1];
        if (!BIOPointer::PrintfAll(out, (j == 0) ? "%X" : ":%X", pair)) {
          return false;
        }
      }
    } else {
#if NCRYPTO_USE_OPENSSL_PROVIDER
      return BIOPointer::PrintfAll(out, "<invalid length=%d>", ip_len);
#elif NCRYPTO_USE_BORINGSSL
      return BIOPointer::WriteAll(out, "<invalid>");
#else
#error "Unsupported crypto backend"
#endif
    }
  } else if (gen->type == GEN_RID) {
    // Unlike OpenSSL's default implementation, never print the OID as text and
    // instead always print its numeric representation.
    auto oid = ObjectToText(gen->d.rid, true);
    return oid.has_value() && BIOPointer::WriteAll(out, "Registered ID:") &&
           BIOPointer::WriteAll(out, oid.value());
  } else if (gen->type == GEN_OTHERNAME) {
    if (gen->d.otherName == nullptr || gen->d.otherName->type_id == nullptr ||
        gen->d.otherName->value == nullptr) {
      return false;
    }
    // The format that is used here is based on OpenSSL's implementation of
    // GENERAL_NAME_print. Earlier versions of Node.js
    // instead produced the same format as i2v_GENERAL_NAME, which was somewhat
    // awkward, especially when passed to translatePeerCertificate.
    bool unicode = true;
    const char* prefix = nullptr;
    // BoringSSL does not support othername in GENERAL_NAME_print and may
    // not define these NIDs.
#if NCRYPTO_USE_OPENSSL_PROVIDER
    int nid = OBJ_obj2nid(gen->d.otherName->type_id);
    switch (nid) {
      case NID_id_on_SmtpUTF8Mailbox:
        prefix = "SmtpUTF8Mailbox";
        break;
      case NID_XmppAddr:
        prefix = "XmppAddr";
        break;
      case NID_SRVName:
        prefix = "SRVName";
        unicode = false;
        break;
      case NID_ms_upn:
        prefix = "UPN";
        break;
      case NID_NAIRealm:
        prefix = "NAIRealm";
        break;
    }
#endif  // NCRYPTO_USE_OPENSSL_PROVIDER
    int val_type = gen->d.otherName->value->type;
    if (prefix == nullptr || (unicode && val_type != V_ASN1_UTF8STRING) ||
        (!unicode && val_type != V_ASN1_IA5STRING)) {
      return BIOPointer::WriteAll(out, "othername:<unsupported>");
    } else {
      if (!BIOPointer::WriteAll(out, "othername:")) return false;
      if (unicode) {
        auto name = gen->d.otherName->value->value.utf8string;
        if (name == nullptr) return false;
        int length = ASN1_STRING_length(name);
        return length >= 0 && PrintAltName(out,
                                           reinterpret_cast<const char*>(
                                               ASN1_STRING_get0_data(name)),
                                           static_cast<size_t>(length),
                                           AltNameOption::UTF8,
                                           prefix);
      } else {
        auto name = gen->d.otherName->value->value.ia5string;
        if (name == nullptr) return false;
        int length = ASN1_STRING_length(name);
        return length >= 0 && PrintAltName(out,
                                           reinterpret_cast<const char*>(
                                               ASN1_STRING_get0_data(name)),
                                           static_cast<size_t>(length),
                                           AltNameOption::NONE,
                                           prefix);
      }
    }
  } else if (gen->type == GEN_X400) {
    // TODO(tniessen): this is what OpenSSL does, implement properly instead
    return BIOPointer::WriteAll(out, "X400Name:<unsupported>");
  } else if (gen->type == GEN_EDIPARTY) {
    // TODO(tniessen): this is what OpenSSL does, implement properly instead
    return BIOPointer::WriteAll(out, "EdiPartyName:<unsupported>");
  } else {
    // This is safe because X509V3_EXT_d2i would have returned nullptr in this
    // case already.
    unreachable();
  }

  return true;
}
}  // namespace

bool SafeX509SubjectAltNamePrint(const BIOPointer& out,
                                 const X509_EXTENSION* ext) {
  if (!out || ext == nullptr) return false;
  // const_cast needed for OpenSSL < 4.0 which lacks const-correctness
  auto* mext = const_cast<X509_EXTENSION*>(ext);
  const ASN1_OBJECT* object = X509_EXTENSION_get_object(mext);
  if (object == nullptr) return false;
  auto ret = OBJ_obj2nid(object);
  if (ret != NID_subject_alt_name) return false;

  GENERAL_NAMES* names = static_cast<GENERAL_NAMES*>(X509V3_EXT_d2i(mext));
  if (names == nullptr) return false;

  const OPENSSL_SIZE_T count = sk_GENERAL_NAME_num(names);
  bool ok = count > 0;

  for (OPENSSL_SIZE_T i = 0; ok && i < count; i++) {
    GENERAL_NAME* gen = sk_GENERAL_NAME_value(names, i);
    if (gen == nullptr) {
      ok = false;
      break;
    }

    if (i != 0 && !(ok = BIOPointer::WriteAll(out, ", "))) {
      break;
    }

    if (!(ok = ncrypto::PrintGeneralName(out, gen))) {
      break;
    }
  }
  sk_GENERAL_NAME_pop_free(names, GENERAL_NAME_free);

  return ok;
}

bool SafeX509InfoAccessPrint(const BIOPointer& out, const X509_EXTENSION* ext) {
  if (!out || ext == nullptr) return false;
  // const_cast needed for OpenSSL < 4.0 which lacks const-correctness
  auto* mext = const_cast<X509_EXTENSION*>(ext);
  const ASN1_OBJECT* object = X509_EXTENSION_get_object(mext);
  if (object == nullptr) return false;
  auto ret = OBJ_obj2nid(object);
  if (ret != NID_info_access) return false;

  AUTHORITY_INFO_ACCESS* descs =
      static_cast<AUTHORITY_INFO_ACCESS*>(X509V3_EXT_d2i(mext));
  if (descs == nullptr) return false;

  const OPENSSL_SIZE_T count = sk_ACCESS_DESCRIPTION_num(descs);
  bool ok = count > 0;

  for (OPENSSL_SIZE_T i = 0; ok && i < count; i++) {
    ACCESS_DESCRIPTION* desc = sk_ACCESS_DESCRIPTION_value(descs, i);
    if (desc == nullptr || desc->method == nullptr ||
        desc->location == nullptr) {
      ok = false;
      break;
    }

    if (i != 0 && !(ok = BIOPointer::WriteAll(out, "\n"))) {
      break;
    }

    auto method = ObjectToText(desc->method, false);
    if (!(ok = method.has_value() &&
               BIOPointer::WriteAll(out, method.value()) &&
               BIOPointer::WriteAll(out, " - "))) {
      break;
    }
    if (!(ok = ncrypto::PrintGeneralName(out, desc->location))) {
      break;
    }
  }
  sk_ACCESS_DESCRIPTION_pop_free(descs, ACCESS_DESCRIPTION_free);

#if NCRYPTO_USE_BORINGSSL
  if (ok && !BIOPointer::WriteAll(out, "\n")) return false;
#endif

  return ok;
}

// ============================================================================
// X509Pointer

X509Pointer::X509Pointer(X509* x509) : cert_(x509) {}

X509Pointer::X509Pointer(X509Pointer&& other) noexcept
    : cert_(other.release()) {}

X509Pointer& X509Pointer::operator=(X509Pointer&& other) noexcept {
  if (this == &other) return *this;
  this->~X509Pointer();
  return *new (this) X509Pointer(std::move(other));
}

X509Pointer::~X509Pointer() {
  reset();
}

void X509Pointer::reset(X509* x509) {
  cert_.reset(x509);
}

X509* X509Pointer::release() {
  return cert_.release();
}

X509View X509Pointer::view() const {
  return X509View(cert_.get());
}

BIOPointer X509View::toPEM() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return {};
  BIOPointer bio(BIO_new(BIO_s_mem()));
  if (!bio) return {};
  if (PEM_write_bio_X509(bio.get(), const_cast<X509*>(cert_)) <= 0) return {};
  return bio;
}

BIOPointer X509View::toDER() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return {};
  BIOPointer bio(BIO_new(BIO_s_mem()));
  if (!bio) return {};
  if (i2d_X509_bio(bio.get(), const_cast<X509*>(cert_)) <= 0) return {};
  return bio;
}

const X509Name X509View::getSubjectName() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return {};
  return X509Name(X509_get_subject_name(cert_));
}

const X509Name X509View::getIssuerName() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return {};
  return X509Name(X509_get_issuer_name(cert_));
}

BIOPointer X509View::getSubject() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return {};
  const X509_NAME* name = X509_get_subject_name(cert_);
  if (name == nullptr) return {};
  BIOPointer bio = BIOPointer::NewMem();
  if (!bio) return {};
  if (X509_NAME_print_ex(bio.get(), name, 0, kX509NameFlagsMultiline) < 0) {
    return {};
  }
  return bio;
}

BIOPointer X509View::getSubjectAltName(bool* has_extension) const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (has_extension != nullptr) *has_extension = false;
  if (cert_ == nullptr) return {};
  int index = X509_get_ext_by_NID(cert_, NID_subject_alt_name, -1);
  if (index < 0) return {};
  if (has_extension != nullptr) *has_extension = true;

  const X509_EXTENSION* extension = X509_get_ext(cert_, index);
  BIOPointer bio = BIOPointer::NewMem();
  if (!bio || extension == nullptr ||
      !SafeX509SubjectAltNamePrint(bio, extension)) {
    return {};
  }
  return bio;
}

BIOPointer X509View::getIssuer() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return {};
  const X509_NAME* name = X509_get_issuer_name(cert_);
  if (name == nullptr) return {};
  BIOPointer bio = BIOPointer::NewMem();
  if (!bio) return {};
  if (X509_NAME_print_ex(bio.get(), name, 0, kX509NameFlagsMultiline) < 0) {
    return {};
  }
  return bio;
}

BIOPointer X509View::getInfoAccess() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return {};
  BIOPointer bio(BIO_new(BIO_s_mem()));
  if (!bio) return {};
  int index = X509_get_ext_by_NID(cert_, NID_info_access, -1);
  if (index < 0) return {};
  const X509_EXTENSION* extension = X509_get_ext(cert_, index);
  if (extension == nullptr || !SafeX509InfoAccessPrint(bio, extension)) {
    return {};
  }
  return bio;
}

namespace {
bool IsX509TimePrintable(const ASN1_TIME* time) {
#if NCRYPTO_USE_BORINGSSL
  // BoringSSL's ASN1_TIME_check() accepts legacy timezone offsets that its
  // ASN1_TIME_print() deliberately rejects. Use the same strict conversion
  // path as its printer so the fallback is selected before writing output.
  int64_t ignored{};
  return ASN1_TIME_to_posix(time, &ignored) == 1;
#elif NCRYPTO_USE_OPENSSL_PROVIDER
  return ASN1_TIME_check(time) == 1;
#else
#error "Unsupported crypto backend"
#endif
}
}  // namespace

BIOPointer X509View::getValidFrom() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return {};
  const ASN1_TIME* time = X509_get0_notBefore(cert_);
  if (time == nullptr) return {};
  BIOPointer bio = BIOPointer::NewMem();
  if (!bio) return {};
  if (IsX509TimePrintable(time)) {
    if (ASN1_TIME_print(bio.get(), time) <= 0) return {};
  } else if (!BIOPointer::WriteAll(bio, "Bad time value")) {
    return {};
  }
  return bio;
}

BIOPointer X509View::getValidTo() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return {};
  const ASN1_TIME* time = X509_get0_notAfter(cert_);
  if (time == nullptr) return {};
  BIOPointer bio = BIOPointer::NewMem();
  if (!bio) return {};
  if (IsX509TimePrintable(time)) {
    if (ASN1_TIME_print(bio.get(), time) <= 0) return {};
  } else if (!BIOPointer::WriteAll(bio, "Bad time value")) {
    return {};
  }
  return bio;
}

std::optional<std::string_view> X509View::getSignatureAlgorithm() const {
  if (cert_ == nullptr) return std::nullopt;
  int nid = X509_get_signature_nid(cert_);
  if (nid == NID_undef) return std::nullopt;
  const char* ln = OBJ_nid2ln(nid);
  if (ln == nullptr) return std::nullopt;
  return std::string_view(ln);
}

std::optional<std::string> X509View::getSignatureAlgorithmOID() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return std::nullopt;
  const X509_ALGOR* alg = nullptr;
  X509_get0_signature(nullptr, &alg, cert_);
  if (alg == nullptr) return std::nullopt;
  const ASN1_OBJECT* obj = nullptr;
  X509_ALGOR_get0(&obj, nullptr, nullptr, alg);
  if (obj == nullptr) return std::nullopt;
  return ObjectToText(obj, true);
}

std::optional<int64_t> X509View::getValidToTime() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return std::nullopt;
  const ASN1_TIME* time = X509_get0_notAfter(cert_);
  if (time == nullptr) return std::nullopt;
#if NCRYPTO_USE_BORINGSSL
  // Boringssl does not implement ASN1_TIME_to_tm in a public way,
  // and only recently added ASN1_TIME_to_posix. Some boringssl
  // users on older version may still need to patch around this
  // or use a different implementation.
  int64_t tp{};
  if (ASN1_TIME_to_posix(time, &tp) != 1) return std::nullopt;
  return tp;
#elif NCRYPTO_USE_OPENSSL_PROVIDER
  struct tm tp {};
  if (ASN1_TIME_to_tm(time, &tp) != 1) return std::nullopt;
  return PortableTimeGM(&tp);
#else
#error "Unsupported crypto backend"
#endif
}

std::optional<int64_t> X509View::getValidFromTime() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return std::nullopt;
  const ASN1_TIME* time = X509_get0_notBefore(cert_);
  if (time == nullptr) return std::nullopt;
#if NCRYPTO_USE_BORINGSSL
  int64_t tp{};
  if (ASN1_TIME_to_posix(time, &tp) != 1) return std::nullopt;
  return tp;
#elif NCRYPTO_USE_OPENSSL_PROVIDER
  struct tm tp {};
  if (ASN1_TIME_to_tm(time, &tp) != 1) return std::nullopt;
  return PortableTimeGM(&tp);
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer X509View::getSerialNumber() const {
  ClearErrorOnReturn clearErrorOnReturn;
  if (cert_ == nullptr) return {};
  if (ASN1_INTEGER* serial_number =
          X509_get_serialNumber(const_cast<X509*>(cert_))) {
    if (auto bn = BignumPointer(ASN1_INTEGER_to_BN(serial_number, nullptr))) {
      return bn.toHex();
    }
  }
  return {};
}

Result<EVPKeyPointer, int> X509View::getPublicKey() const {
  ClearErrorOnReturn clearErrorOnReturn;
  if (cert_ == nullptr) return Result<EVPKeyPointer, int>(EVPKeyPointer{});
  auto pkey = EVPKeyPointer(X509_get_pubkey(const_cast<X509*>(cert_)));
  if (!pkey) return Result<EVPKeyPointer, int>(ERR_get_error());
  return pkey;
}

StackOfASN1 X509View::getKeyUsage() const {
  ClearErrorOnReturn clearErrorOnReturn;
  if (cert_ == nullptr) return {};
  return StackOfASN1(static_cast<STACK_OF(ASN1_OBJECT)*>(
      X509_get_ext_d2i(cert_, NID_ext_key_usage, nullptr, nullptr)));
}

bool X509View::isCA() const {
  ClearErrorOnReturn clearErrorOnReturn;
  if (cert_ == nullptr) return false;
  return X509_check_ca(const_cast<X509*>(cert_)) == 1;
}

namespace {
bool CopyStringBytes(const ASN1_STRING* value,
                     std::vector<unsigned char>* output) {
  if (value == nullptr) return false;
  const int length = ASN1_STRING_length(value);
  const unsigned char* data = ASN1_STRING_get0_data(value);
  if (length < 0 || (length > 0 && data == nullptr)) return false;
  if (length == 0) {
    output->clear();
  } else {
    output->assign(data, data + static_cast<size_t>(length));
  }
  return true;
}

bool CanConvertNameValueToUtf8(const ASN1_STRING* value) {
  if (value == nullptr) return false;
  switch (ASN1_STRING_type(value)) {
    case V_ASN1_UTF8STRING:
    case V_ASN1_NUMERICSTRING:
    case V_ASN1_PRINTABLESTRING:
    case V_ASN1_T61STRING:
    case V_ASN1_IA5STRING:
    case V_ASN1_UTCTIME:
    case V_ASN1_GENERALIZEDTIME:
    case V_ASN1_ISO64STRING:
    case V_ASN1_UNIVERSALSTRING:
    case V_ASN1_BMPSTRING:
      return true;
    default:
      return false;
  }
}

std::optional<size_t> GetEncodedRdnCount(const X509_NAME* name) {
  const unsigned char* der = nullptr;
  size_t der_length = 0;
  if (X509_NAME_get0_der(name, &der, &der_length) != 1 || der == nullptr ||
      der_length > static_cast<size_t>(LONG_MAX)) {
    return std::nullopt;
  }

  const unsigned char* cursor = der;
  ASN1SequencePointer rdns(d2i_ASN1_SEQUENCE_ANY(
      nullptr, &cursor, static_cast<long>(der_length)));  // NOLINT(runtime/int)
  if (!rdns || cursor != der + der_length) return std::nullopt;

  const OPENSSL_SIZE_T count = sk_ASN1_TYPE_num(rdns.get());
  for (OPENSSL_SIZE_T i = 0; i < count; i++) {
    const ASN1_TYPE* rdn = sk_ASN1_TYPE_value(rdns.get(), i);
    if (rdn == nullptr || ASN1_TYPE_get(rdn) != V_ASN1_SET) {
      return std::nullopt;
    }
  }
  return static_cast<size_t>(count);
}

std::optional<X509NameInspection> InspectName(const X509_NAME* name) {
  if (name == nullptr) return std::nullopt;
  const int count = X509_NAME_entry_count(name);
  if (count < 0) return std::nullopt;
  const auto encoded_rdn_count = GetEncodedRdnCount(name);
  if (!encoded_rdn_count.has_value()) return std::nullopt;

  X509NameInspection output;
  output.reserve(static_cast<size_t>(count));
  int previous_set = -1;
  for (int i = 0; i < count; i++) {
    const X509_NAME_ENTRY* entry = X509_NAME_get_entry(name, i);
    if (entry == nullptr) return std::nullopt;

    const ASN1_OBJECT* object = X509_NAME_ENTRY_get_object(entry);
    const ASN1_STRING* value = X509_NAME_ENTRY_get_data(entry);
    auto oid = ObjectToText(object, true);
    const int set = X509_NAME_ENTRY_set(entry);
    if (!oid.has_value() || value == nullptr || set < 0 ||
        (set != previous_set &&
         (previous_set == INT_MAX || set != previous_set + 1))) {
      return std::nullopt;
    }
    previous_set = set;

    X509NameAttribute attribute{
        .set = set,
        .oid = std::move(oid.value()),
    };
    if (CanConvertNameValueToUtf8(value)) {
      unsigned char* data = nullptr;
      const int length = ASN1_STRING_to_UTF8(&data, value);
      DataPointer owned(data, length > 0 ? static_cast<size_t>(length) : 0);
      if (length < 0 || (length > 0 && !owned)) return std::nullopt;
      attribute.value =
          length == 0 ? std::string()
                      : std::string(reinterpret_cast<const char*>(owned.get()),
                                    static_cast<size_t>(length));
    }
    output.push_back(std::move(attribute));
  }
  const size_t inspected_rdn_count =
      previous_set < 0 ? 0 : static_cast<size_t>(previous_set) + 1;
  if (inspected_rdn_count != encoded_rdn_count.value()) return std::nullopt;
  return output;
}

std::optional<std::vector<X509RawExtension>> InspectExtensions(
    const X509* certificate) {
  if (certificate == nullptr) return std::nullopt;
  const int count = X509_get_ext_count(certificate);
  if (count < 0) return std::nullopt;

  std::vector<X509RawExtension> output;
  output.reserve(static_cast<size_t>(count));
  for (int i = 0; i < count; i++) {
    const X509_EXTENSION* extension = X509_get_ext(certificate, i);
    if (extension == nullptr) return std::nullopt;

    auto* mutable_extension = const_cast<X509_EXTENSION*>(extension);
    auto oid = ObjectToText(X509_EXTENSION_get_object(mutable_extension), true);
    const ASN1_OCTET_STRING* value = X509_EXTENSION_get_data(mutable_extension);
    if (!oid.has_value() || value == nullptr) return std::nullopt;

    X509RawExtension inspected{
        .oid = std::move(oid.value()),
        .critical = X509_EXTENSION_get_critical(mutable_extension) != 0,
    };
    if (!CopyStringBytes(value, &inspected.value)) return std::nullopt;
    output.push_back(std::move(inspected));
  }
  return output;
}
}  // namespace

std::optional<int64_t> X509View::getVersion() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return std::nullopt;
  const long version = X509_get_version(cert_);  // NOLINT(runtime/int)
  if (version < 0 || version > 2) return std::nullopt;
  return static_cast<int64_t>(version) + 1;
}

std::optional<X509NameInspection> X509View::inspectSubjectName() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return std::nullopt;
  return InspectName(X509_get_subject_name(cert_));
}

std::optional<X509NameInspection> X509View::inspectIssuerName() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return std::nullopt;
  return InspectName(X509_get_issuer_name(cert_));
}

std::optional<std::vector<X509RawExtension>> X509View::inspectExtensions()
    const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  return InspectExtensions(cert_);
}

bool X509View::isIssuedBy(const X509View& issuer) const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr || issuer.cert_ == nullptr) return false;
  return X509_check_issued(const_cast<X509*>(issuer.cert_),
                           const_cast<X509*>(cert_)) == X509_V_OK;
}

bool X509View::checkPrivateKey(const EVPKeyPointer& pkey) const {
  ClearErrorOnReturn clearErrorOnReturn;
  if (cert_ == nullptr || pkey == nullptr) return false;
  return X509_check_private_key(const_cast<X509*>(cert_), pkey.get()) == 1;
}

bool X509View::checkPublicKey(const EVPKeyPointer& pkey) const {
  ClearErrorOnReturn clearErrorOnReturn;
  if (cert_ == nullptr || pkey == nullptr) return false;
  return X509_verify(const_cast<X509*>(cert_), pkey.get()) == 1;
}

X509View::CheckMatch X509View::checkHost(const std::string_view host,
                                         int flags,
                                         DataPointer* peerName) const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return CheckMatch::NO_MATCH;
  char* peername;
  switch (X509_check_host(
      const_cast<X509*>(cert_), host.data(), host.size(), flags, &peername)) {
    case 0:
      return CheckMatch::NO_MATCH;
    case 1: {
      if (peername != nullptr) {
        DataPointer name(peername, strlen(peername));
        if (peerName != nullptr) *peerName = std::move(name);
      }
      return CheckMatch::MATCH;
    }
    case -2:
      return CheckMatch::INVALID_NAME;
    default:
      return CheckMatch::OPERATION_FAILED;
  }
}

X509View::CheckMatch X509View::checkEmail(const std::string_view email,
                                          int flags) const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return CheckMatch::NO_MATCH;
  switch (X509_check_email(
      const_cast<X509*>(cert_), email.data(), email.size(), flags)) {
    case 0:
      return CheckMatch::NO_MATCH;
    case 1:
      return CheckMatch::MATCH;
    case -2:
      return CheckMatch::INVALID_NAME;
    default:
      return CheckMatch::OPERATION_FAILED;
  }
}

X509View::CheckMatch X509View::checkIp(const Buffer<const unsigned char>& ip,
                                       int flags) const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (cert_ == nullptr) return CheckMatch::NO_MATCH;
  if (ip.len != 4 && ip.len != 16) return CheckMatch::INVALID_NAME;
  switch (X509_check_ip(const_cast<X509*>(cert_), ip.data, ip.len, flags)) {
    case 0:
      return CheckMatch::NO_MATCH;
    case 1:
      return CheckMatch::MATCH;
    case -2:
      return CheckMatch::INVALID_NAME;
    default:
      return CheckMatch::OPERATION_FAILED;
  }
}

X509View X509View::From(const SSLPointer& ssl) {
  ClearErrorOnReturn clear_error_on_return;
  if (!ssl) return {};
  return X509View(SSL_get_certificate(ssl.get()));
}

X509View X509View::From(const SSLCtxPointer& ctx) {
  ClearErrorOnReturn clear_error_on_return;
  if (!ctx) return {};
  return X509View(SSL_CTX_get0_certificate(ctx.get()));
}

std::optional<std::string> X509View::getFingerprint(
    const Digest& method) const {
  unsigned int md_size;
  unsigned char md[EVP_MAX_MD_SIZE];
  static constexpr char hex[] = "0123456789ABCDEF";

  if (X509_digest(get(), method, md, &md_size)) {
    if (md_size == 0) return std::nullopt;
    std::string fingerprint((md_size * 3) - 1, 0);
    for (unsigned int i = 0; i < md_size; i++) {
      auto idx = 3 * i;
      fingerprint[idx] = hex[(md[i] & 0xf0) >> 4];
      fingerprint[idx + 1] = hex[(md[i] & 0x0f)];
      if (i == md_size - 1) break;
      fingerprint[idx + 2] = ':';
    }

    return fingerprint;
  }

  return std::nullopt;
}

X509Pointer X509View::clone() const {
  ClearErrorOnReturn clear_error_on_return;
  if (!cert_) return {};
  return X509Pointer(X509_dup(const_cast<X509*>(cert_)));
}

Result<X509Pointer, int> X509Pointer::Parse(
    Buffer<const unsigned char> buffer) {
  ClearErrorOnReturn clearErrorOnReturn;
  BIOPointer bio(BIO_new_mem_buf(buffer.data, buffer.len));
  if (!bio) return Result<X509Pointer, int>(ERR_get_error());

  X509Pointer pem(
      PEM_read_bio_X509_AUX(bio.get(), nullptr, NoPasswordCallback, nullptr));
  if (pem) return Result<X509Pointer, int>(std::move(pem));
  BIO_reset(bio.get());

  X509Pointer der(d2i_X509_bio(bio.get(), nullptr));
  if (der) return Result<X509Pointer, int>(std::move(der));

  return Result<X509Pointer, int>(ERR_get_error());
}

bool X509View::enumUsages(UsageCallback callback) const {
  if (cert_ == nullptr) return false;
  StackOfASN1 eku;
  {
    MarkPopErrorOnReturn mark_pop_error_on_return;
    eku.reset(static_cast<STACK_OF(ASN1_OBJECT)*>(
        X509_get_ext_d2i(cert_, NID_ext_key_usage, nullptr, nullptr)));
  }
  if (!eku) return false;
  const int count = sk_ASN1_OBJECT_num(eku.get());

  for (int i = 0; i < count; i++) {
    std::optional<std::string> usage;
    {
      MarkPopErrorOnReturn mark_pop_error_on_return;
      usage = ObjectToText(sk_ASN1_OBJECT_value(eku.get(), i), true);
    }
    if (!usage.has_value()) return false;
    callback(usage->c_str());
  }
  return true;
}

bool X509View::ifRsa(KeyCallback<Rsa> callback) const {
  if (cert_ == nullptr) return true;
  OSSL3_CONST EVP_PKEY* pkey;
  {
    MarkPopErrorOnReturn mark_pop_error_on_return;
    pkey = X509_get0_pubkey(cert_);
  }
  if (pkey == nullptr) return true;
  if (EVPKeyPointer::isRsaVariant(pkey)) {
    std::optional<Rsa> rsa;
    {
      MarkPopErrorOnReturn mark_pop_error_on_return;
#if NCRYPTO_USE_OPENSSL_PROVIDER
      rsa.emplace(pkey);
#elif NCRYPTO_USE_BORINGSSL
      rsa.emplace(EVP_PKEY_get0_RSA(pkey));
#else
#error "Unsupported crypto backend"
#endif
    }
    if (!*rsa) [[unlikely]]
      return true;
    return callback(*rsa);
  }
  return true;
}

bool X509View::ifEc(KeyCallback<Ec> callback) const {
  if (cert_ == nullptr) return true;
  OSSL3_CONST EVP_PKEY* pkey;
  {
    MarkPopErrorOnReturn mark_pop_error_on_return;
    pkey = X509_get0_pubkey(cert_);
  }
  if (pkey == nullptr) return true;
  if (EVPKeyPointer::isA(pkey, KeyAlgorithm::EC)) {
    std::optional<Ec> ec;
    {
      MarkPopErrorOnReturn mark_pop_error_on_return;
#if NCRYPTO_USE_OPENSSL_PROVIDER
      ec.emplace(pkey);
#elif NCRYPTO_USE_BORINGSSL
      ec.emplace(EVP_PKEY_get0_EC_KEY(pkey));
#else
#error "Unsupported crypto backend"
#endif
    }
    if (!*ec) [[unlikely]]
      return true;
    return callback(*ec);
  }
  return true;
}

X509Pointer X509Pointer::IssuerFrom(const SSLPointer& ssl,
                                    const X509View& view) {
  return IssuerFrom(SSL_get_SSL_CTX(ssl.get()), view);
}

X509Pointer X509Pointer::IssuerFrom(const SSL_CTX* ctx, const X509View& cert) {
  X509_STORE* store = SSL_CTX_get_cert_store(ctx);
  DeleteFnPtr<X509_STORE_CTX, X509_STORE_CTX_free> store_ctx(
      X509_STORE_CTX_new());
  X509Pointer result;
  X509* issuer;
  if (store_ctx.get() != nullptr &&
      X509_STORE_CTX_init(store_ctx.get(), store, nullptr, nullptr) == 1 &&
      X509_STORE_CTX_get1_issuer(&issuer, store_ctx.get(), cert.get()) == 1) {
    result.reset(issuer);
  }
  return result;
}

X509Pointer X509Pointer::PeerFrom(const SSLPointer& ssl) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return X509Pointer(SSL_get1_peer_certificate(ssl.get()));
#elif NCRYPTO_USE_BORINGSSL
  return X509Pointer(SSL_get_peer_certificate(ssl.get()));
#else
#error "Unsupported crypto backend"
#endif
}

// When adding or removing errors below, please also update the list in the API
// documentation. See the "OpenSSL Error Codes" section of doc/api/errors.md
// Also *please* update the respective section in doc/api/tls.md as well
const char* X509Pointer::ErrorCode(int32_t err) {  // NOLINT(runtime/int)
#define CASE(CODE)                                                             \
  case X509_V_ERR_##CODE:                                                      \
    return #CODE;
  switch (err) {
    CASE(UNABLE_TO_GET_ISSUER_CERT)
    CASE(UNABLE_TO_GET_CRL)
    CASE(UNABLE_TO_DECRYPT_CERT_SIGNATURE)
    CASE(UNABLE_TO_DECRYPT_CRL_SIGNATURE)
    CASE(UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY)
    CASE(CERT_SIGNATURE_FAILURE)
    CASE(CRL_SIGNATURE_FAILURE)
    CASE(CERT_NOT_YET_VALID)
    CASE(CERT_HAS_EXPIRED)
    CASE(CRL_NOT_YET_VALID)
    CASE(CRL_HAS_EXPIRED)
    CASE(ERROR_IN_CERT_NOT_BEFORE_FIELD)
    CASE(ERROR_IN_CERT_NOT_AFTER_FIELD)
    CASE(ERROR_IN_CRL_LAST_UPDATE_FIELD)
    CASE(ERROR_IN_CRL_NEXT_UPDATE_FIELD)
    CASE(OUT_OF_MEM)
    CASE(DEPTH_ZERO_SELF_SIGNED_CERT)
    CASE(SELF_SIGNED_CERT_IN_CHAIN)
    CASE(UNABLE_TO_GET_ISSUER_CERT_LOCALLY)
    CASE(UNABLE_TO_VERIFY_LEAF_SIGNATURE)
    CASE(CERT_CHAIN_TOO_LONG)
    CASE(CERT_REVOKED)
    CASE(INVALID_CA)
    CASE(PATH_LENGTH_EXCEEDED)
    CASE(INVALID_PURPOSE)
    CASE(CERT_UNTRUSTED)
    CASE(CERT_REJECTED)
    CASE(HOSTNAME_MISMATCH)
  }
#undef CASE
  return "UNSPECIFIED";
}

std::optional<const char*> X509Pointer::ErrorReason(int32_t err) {
  if (err == X509_V_OK) return std::nullopt;
  return X509_verify_cert_error_string(err);
}

// ============================================================================
// BIOPointer

BIOPointer::BIOPointer(BIO* bio) : bio_(bio) {}

BIOPointer::BIOPointer(BIOPointer&& other) noexcept : bio_(other.release()) {}

BIOPointer& BIOPointer::operator=(BIOPointer&& other) noexcept {
  if (this == &other) return *this;
  this->~BIOPointer();
  return *new (this) BIOPointer(std::move(other));
}

BIOPointer::~BIOPointer() {
  reset();
}

void BIOPointer::reset(BIO* bio) {
  bio_.reset(bio);
}

BIO* BIOPointer::release() {
  return bio_.release();
}

bool BIOPointer::resetBio() const {
  if (!bio_) return 0;
  return BIO_reset(bio_.get()) == 1;
}

BIOPointer BIOPointer::NewMem() {
  return BIOPointer(BIO_new(BIO_s_mem()));
}

BIOPointer BIOPointer::NewSecMem() {
#ifdef OPENSSL_IS_BORINGSSL
  // Boringssl does not implement the BIO_s_secmem API.
  return BIOPointer(BIO_new(BIO_s_mem()));
#else
  return BIOPointer(BIO_new(BIO_s_secmem()));
#endif
}

BIOPointer BIOPointer::New(const BIO_METHOD* method) {
  if (method == nullptr) return {};
  return BIOPointer(BIO_new(method));
}

BIOPointer BIOPointer::New(const void* data, size_t len) {
  return BIOPointer(BIO_new_mem_buf(data, len));
}

BIOPointer BIOPointer::NewFile(const char* filename, const char* mode) {
  return BIOPointer(BIO_new_file(filename, mode));
}

BIOPointer BIOPointer::NewFp(FILE* fd, int close_flag) {
  return BIOPointer(BIO_new_fp(fd, close_flag));
}

BIOPointer BIOPointer::New(const BIGNUM* bn) {
  auto res = NewMem();
  if (!res || !BN_print(res.get(), bn)) return {};
  return res;
}

int BIOPointer::Write(BIOPointer* bio, std::string_view message) {
  if (bio == nullptr || !*bio) return 0;
  return BIO_write(bio->get(), message.data(), message.size());
}

bool BIOPointer::WriteAll(const BIOPointer& bio, std::string_view message) {
  if (!bio) return false;
  while (!message.empty()) {
    const size_t chunk_size =
        std::min(message.size(), static_cast<size_t>(INT_MAX));
    const int length = static_cast<int>(chunk_size);
    if (BIO_write(bio.get(), message.data(), length) != length) return false;
    message.remove_prefix(chunk_size);
  }
  return true;
}

// ============================================================================
// DHPointer

namespace {
#if NCRYPTO_USE_OPENSSL_PROVIDER
const char* GetOpenSSLDhGroupName(const std::string_view name,
                                  DHPointer::FindGroupOption option) {
  if (option != DHPointer::FindGroupOption::NO_SMALL_PRIMES &&
      CaseInsensitiveNameEqual()(name, "modp5")) {
    return "modp_1536";
  }
  if (CaseInsensitiveNameEqual()(name, "modp14")) return "modp_2048";
  if (CaseInsensitiveNameEqual()(name, "modp15")) return "modp_3072";
  if (CaseInsensitiveNameEqual()(name, "modp16")) return "modp_4096";
  if (CaseInsensitiveNameEqual()(name, "modp17")) return "modp_6144";
  if (CaseInsensitiveNameEqual()(name, "modp18")) return "modp_8192";
  return nullptr;
}

int GetDhGroupPrivateBits(const char* group_name) {
  if (group_name == nullptr) return 0;
  if (strcmp(group_name, "modp_1536") == 0) return 200;
  if (strcmp(group_name, "modp_2048") == 0) return 225;
  if (strcmp(group_name, "modp_3072") == 0) return 275;
  if (strcmp(group_name, "modp_4096") == 0) return 325;
  if (strcmp(group_name, "modp_6144") == 0) return 375;
  if (strcmp(group_name, "modp_8192") == 0) return 400;
  return 0;
}

bool GenerateDhPrivateKey(BignumPointer* out,
                          const BIGNUM* p,
                          const char* group_name) {
  if (out == nullptr || p == nullptr) return false;
  auto priv = BignumPointer::NewSecure();
  if (!priv) return false;

  const int bits = GetDhGroupPrivateBits(group_name);
  if (bits > 0) {
    if (BN_priv_rand(priv.get(), bits, BN_RAND_TOP_ONE, BN_RAND_BOTTOM_ANY) !=
        1) {
      return false;
    }
  } else {
    auto range = BignumPointer(BN_dup(p));
    if (!range || BN_sub_word(range.get(), 3) != 1 ||
        BN_priv_rand_range(priv.get(), range.get()) != 1 ||
        BN_add_word(priv.get(), 2) != 1) {
      return false;
    }
  }

  *out = std::move(priv);
  return true;
}

// Recompute DH public keys locally when a private key already exists. Provider
// keygen creates a fresh keypair, which is both slower and changes semantics.
bool GenerateDhPublicKey(BignumPointer* out,
                         const BIGNUM* p,
                         const BIGNUM* g,
                         const BIGNUM* priv) {
  if (out == nullptr || p == nullptr || g == nullptr || priv == nullptr) {
    return false;
  }
  auto pub = BignumPointer::New();
  BignumCtxPointer ctx(BN_CTX_new());
  if (!pub || !ctx ||
      BN_mod_exp_mont_consttime(pub.get(), g, priv, p, ctx.get(), nullptr) !=
          1) {
    return false;
  }

  *out = std::move(pub);
  return true;
}

std::optional<int> CheckDhParams(const BIGNUM* p,
                                 const BIGNUM* g,
                                 const BIGNUM* q,
                                 const BIGNUM* j) {
  // TODO(panva): In a semver-major, consider tightening OpenSSL validation
  // to report generator and q failures as strictly as legacy DH_check().
  if (p == nullptr || g == nullptr) return std::nullopt;

  const int p_bits = BN_num_bits(p);
  if (p_bits > OPENSSL_DH_CHECK_MAX_MODULUS_BITS) return std::nullopt;

  int codes = 0;
  if (!BN_is_odd(p)) {
    codes |= static_cast<int>(DHPointer::CheckResult::P_NOT_PRIME);
  }
  if (BN_is_negative(g) || BN_is_zero(g) || BN_is_one(g)) {
    codes |= static_cast<int>(DHPointer::CheckResult::NOT_SUITABLE_GENERATOR);
  }
  if (p_bits < 512) {
    codes |= static_cast<int>(DHPointer::CheckResult::MODULUS_TOO_SMALL);
  }
  if (p_bits > OPENSSL_DH_MAX_MODULUS_BITS) {
    codes |= static_cast<int>(DHPointer::CheckResult::MODULUS_TOO_LARGE);
  }

  BignumCtxPointer ctx(BN_CTX_new());
  if (!ctx) return std::nullopt;

  auto tmp1 = BignumPointer::New();
  auto tmp2 = BignumPointer::New();
  if (!tmp1 || !tmp2) return std::nullopt;

  if (BN_copy(tmp1.get(), p) == nullptr || BN_sub_word(tmp1.get(), 1) != 1) {
    return std::nullopt;
  }
  if (BN_cmp(g, tmp1.get()) >= 0) {
    codes |= static_cast<int>(DHPointer::CheckResult::NOT_SUITABLE_GENERATOR);
  }

  bool q_good = false;
  if (q != nullptr) {
    if (BN_ucmp(p, q) > 0) {
      q_good = true;
    } else {
      codes |= static_cast<int>(DHPointer::CheckResult::INVALID_Q);
    }
  }

  if (q_good) {
    if (BN_cmp(g, BN_value_one()) <= 0 || BN_cmp(g, p) >= 0) {
      codes |= static_cast<int>(DHPointer::CheckResult::NOT_SUITABLE_GENERATOR);
    } else if (BN_mod_exp(tmp1.get(), g, q, p, ctx.get()) != 1) {
      return std::nullopt;
    } else if (!BN_is_one(tmp1.get())) {
      codes |= static_cast<int>(DHPointer::CheckResult::NOT_SUITABLE_GENERATOR);
    }

    const int q_is_prime = BN_check_prime(q, ctx.get(), nullptr);
    if (q_is_prime < 0) return std::nullopt;
    if (q_is_prime == 0) {
      codes |= static_cast<int>(DHPointer::CheckResult::Q_NOT_PRIME);
    }

    if (BN_div(tmp1.get(), tmp2.get(), p, q, ctx.get()) != 1) {
      return std::nullopt;
    }
    if (!BN_is_one(tmp2.get())) {
      codes |= static_cast<int>(DHPointer::CheckResult::INVALID_Q);
    }
    if (j != nullptr && BN_cmp(j, tmp1.get()) != 0) {
      codes |= static_cast<int>(DHPointer::CheckResult::INVALID_J);
    }
  }

  const int p_is_prime = BN_check_prime(p, ctx.get(), nullptr);
  if (p_is_prime < 0) return std::nullopt;
  if (p_is_prime == 0) {
    codes |= static_cast<int>(DHPointer::CheckResult::P_NOT_PRIME);
  } else if (q == nullptr) {
    if (BN_rshift1(tmp1.get(), p) != 1) return std::nullopt;
    const int q_is_prime = BN_check_prime(tmp1.get(), ctx.get(), nullptr);
    if (q_is_prime < 0) return std::nullopt;
    if (q_is_prime == 0) {
      codes |= static_cast<int>(DHPointer::CheckResult::P_NOT_SAFE_PRIME);
    }
  }

  return codes;
}
#endif
}  // namespace

#if NCRYPTO_USE_OPENSSL_PROVIDER
DHPointer::DHPointer(EVPKeyPointer&& key, const char* group_name)
    : dh_(key.release()), group_name_(group_name) {}

DHPointer::DHPointer(BignumPointer&& p,
                     BignumPointer&& g,
                     const char* group_name)
    : p_(std::move(p)), g_(std::move(g)), group_name_(group_name) {}
#elif NCRYPTO_USE_BORINGSSL
DHPointer::DHPointer(DH* dh) : dh_(dh) {}
#else
#error "Unsupported crypto backend"
#endif

DHPointer::DHPointer(DHPointer&& other) noexcept
#if NCRYPTO_USE_OPENSSL_PROVIDER
    : dh_(other.dh_.release()),
      p_(std::move(other.p_)),
      g_(std::move(other.g_)),
      pub_key_(std::move(other.pub_key_)),
      pvt_key_(std::move(other.pvt_key_)),
      group_name_(other.group_name_) {
  other.group_name_ = nullptr;
}
#elif NCRYPTO_USE_BORINGSSL
    : dh_(other.release()) {
}
#else
#error "Unsupported crypto backend"
#endif

DHPointer& DHPointer::operator=(DHPointer&& other) noexcept {
  if (this == &other) return *this;
  this->~DHPointer();
  return *new (this) DHPointer(std::move(other));
}

DHPointer::~DHPointer() {
  reset();
}

void DHPointer::reset(
#if NCRYPTO_USE_OPENSSL_PROVIDER
    EVP_PKEY* dh
#elif NCRYPTO_USE_BORINGSSL
    DH* dh
#else
#error "Unsupported crypto backend"
#endif
) {
  dh_.reset(dh);
#if NCRYPTO_USE_OPENSSL_PROVIDER
  p_.reset();
  g_.reset();
  pub_key_.reset();
  pvt_key_.reset();
  group_name_ = nullptr;
#endif
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
EVP_PKEY* DHPointer::release() {
  if (!dh_ && p_ && g_) {
    auto pkey =
        group_name_ != nullptr
            ? NewDhPKey(group_name_, pub_key_.get(), pvt_key_.get())
            : NewDhPKey(p_.get(), g_.get(), pub_key_.get(), pvt_key_.get());
    if (!pkey) return nullptr;
    dh_.reset(pkey.release());
  }
  p_.reset();
  g_.reset();
  pub_key_.reset();
  pvt_key_.reset();
  group_name_ = nullptr;
  return dh_.release();
}
#elif NCRYPTO_USE_BORINGSSL
DH* DHPointer::release() {
  return dh_.release();
}
#else
#error "Unsupported crypto backend"
#endif

BignumPointer DHPointer::FindGroup(const std::string_view name,
                                   FindGroupOption option) {
#define V(n, p)                                                                \
  if (CaseInsensitiveNameEqual()(name, n)) return BignumPointer(p(nullptr));
  if (option != FindGroupOption::NO_SMALL_PRIMES) {
#ifndef OPENSSL_IS_BORINGSSL
    // Boringssl does not support the 768 and 1024 small primes
    V("modp1", BN_get_rfc2409_prime_768);
    V("modp2", BN_get_rfc2409_prime_1024);
#endif
    V("modp5", BN_get_rfc3526_prime_1536);
  }
  V("modp14", BN_get_rfc3526_prime_2048);
  V("modp15", BN_get_rfc3526_prime_3072);
  V("modp16", BN_get_rfc3526_prime_4096);
  V("modp17", BN_get_rfc3526_prime_6144);
  V("modp18", BN_get_rfc3526_prime_8192);
#undef V
  return {};
}

BignumPointer DHPointer::GetStandardGenerator() {
  auto bn = BignumPointer::New();
  if (!bn) return {};
  if (!bn.setWord(2)) return {};
  return bn;
}

DHPointer DHPointer::FromGroup(const std::string_view name,
                               FindGroupOption option) {
  auto group = FindGroup(name, option);
  if (!group) return {};  // Unable to find the named group.

  auto generator = GetStandardGenerator();
  if (!generator) return {};  // Unable to create the generator.

#if NCRYPTO_USE_OPENSSL_PROVIDER
  const char* group_name = GetOpenSSLDhGroupName(name, option);
  return DHPointer(std::move(group), std::move(generator), group_name);
#elif NCRYPTO_USE_BORINGSSL
  return New(std::move(group), std::move(generator));
#else
#error "Unsupported crypto backend"
#endif
}

DHPointer DHPointer::New(BignumPointer&& p, BignumPointer&& g) {
  if (!p || !g) return {};

#if NCRYPTO_USE_OPENSSL_PROVIDER
  auto pkey = NewDhPKey(p.get(), g.get());
  if (!pkey) return {};
  return DHPointer(std::move(pkey));
#elif NCRYPTO_USE_BORINGSSL
  DHPointer dh(DH_new());
  if (!dh) return {};

  if (DH_set0_pqg(dh.get(), p.get(), nullptr, g.get()) != 1) return {};

  // If the call above is successful, the DH object takes ownership of the
  // BIGNUMs, so we must release them here. Unfortunately coverity does not
  // know that so we need to tell it not to complain.
  // coverity[resource_leak]
  p.release();
  // coverity[resource_leak]
  g.release();

  return dh;
#else
#error "Unsupported crypto backend"
#endif
}

DHPointer DHPointer::New(size_t bits, unsigned int generator) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  auto param_ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::DH);
  if (!param_ctx.initForParamgen() ||
      !param_ctx.setDhParameters(bits, generator)) {
    return {};
  }

  auto key_params = param_ctx.paramgen();
  if (!key_params) return {};
  return DHPointer(std::move(key_params));
#elif NCRYPTO_USE_BORINGSSL
  DHPointer dh(DH_new());
  if (!dh) return {};

  if (DH_generate_parameters_ex(dh.get(), bits, generator, nullptr) != 1) {
    return {};
  }

  return dh;
#else
#error "Unsupported crypto backend"
#endif
}

DHPointer::CheckResult DHPointer::check() {
  ClearErrorOnReturn clearErrorOnReturn;
  if (!*this) return DHPointer::CheckResult::NONE;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  // TODO(panva): In a semver-major, consider validating named DH groups
  // through the provider instead of preserving the historical verifyError.
  if (group_name_ != nullptr) return CheckResult::NONE;

  // Like DH_check(), skip primality checks for groups OpenSSL recognizes from
  // explicitly supplied parameters.
  size_t group_name_len = 0;
  if (dh_ &&
      EVP_PKEY_get_group_name(dh_.get(), nullptr, 0, &group_name_len) == 1 &&
      group_name_len > 0) {
    return CheckResult::NONE;
  }

  DeleteFnPtr<BIGNUM, BN_free> p;
  DeleteFnPtr<BIGNUM, BN_free> g;
  DeleteFnPtr<BIGNUM, BN_free> q;
  DeleteFnPtr<BIGNUM, BN_free> j;
  const BIGNUM* p_bn = p_.get();
  const BIGNUM* g_bn = g_.get();
  const BIGNUM* q_bn = nullptr;
  const BIGNUM* j_bn = nullptr;
  if ((p_bn == nullptr || g_bn == nullptr) &&
      !GetDhParams(dh_.get(), &p, &g, &q, &j)) {
    return DHPointer::CheckResult::CHECK_FAILED;
  }
  if (p_bn == nullptr) p_bn = p.get();
  if (g_bn == nullptr) g_bn = g.get();
  q_bn = q.get();
  j_bn = j.get();
  if (p_bn == nullptr || g_bn == nullptr) {
    return DHPointer::CheckResult::CHECK_FAILED;
  }

  auto codes = CheckDhParams(p_bn, g_bn, q_bn, j_bn);
  if (!codes) return DHPointer::CheckResult::CHECK_FAILED;
  return static_cast<CheckResult>(*codes);
#elif NCRYPTO_USE_BORINGSSL
  int codes = 0;
  if (DH_check(dh_.get(), &codes) != 1)
    return DHPointer::CheckResult::CHECK_FAILED;
  return static_cast<CheckResult>(codes);
#else
#error "Unsupported crypto backend"
#endif
}

DHPointer::CheckPublicKeyResult DHPointer::checkPublicKey(
    const BignumPointer& pub_key) {
  ClearErrorOnReturn clearErrorOnReturn;
  if (!pub_key || !*this) {
    return DHPointer::CheckPublicKeyResult::CHECK_FAILED;
  }
#if NCRYPTO_USE_OPENSSL_PROVIDER
  DeleteFnPtr<BIGNUM, BN_free> p;
  DeleteFnPtr<BIGNUM, BN_free> g;
  const BIGNUM* p_bn = p_.get();
  const BIGNUM* g_bn = g_.get();
  if ((p_bn == nullptr || g_bn == nullptr) && !GetDhParams(dh_.get(), &p, &g)) {
    return DHPointer::CheckPublicKeyResult::CHECK_FAILED;
  }
  if (p_bn == nullptr) p_bn = p.get();
  if (g_bn == nullptr) g_bn = g.get();
  if (p_bn == nullptr || g_bn == nullptr) {
    return DHPointer::CheckPublicKeyResult::CHECK_FAILED;
  }

  if (BN_cmp(pub_key.get(), BN_value_one()) <= 0) {
    return DHPointer::CheckPublicKeyResult::TOO_SMALL;
  }

  DeleteFnPtr<BIGNUM, BN_free> p_minus_one(BN_dup(p_bn));
  if (!p_minus_one || BN_sub_word(p_minus_one.get(), 1) != 1) {
    return DHPointer::CheckPublicKeyResult::CHECK_FAILED;
  }

  if (BN_cmp(pub_key.get(), p_minus_one.get()) >= 0) {
    return DHPointer::CheckPublicKeyResult::TOO_LARGE;
  }

  if (p_) {
    if (group_name_ == nullptr) return CheckPublicKeyResult::NONE;

    auto peer = NewDhPKey(group_name_, pub_key.get());
    if (!peer) return DHPointer::CheckPublicKeyResult::CHECK_FAILED;
    EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(peer.get(), nullptr));
    if (!ctx) return DHPointer::CheckPublicKeyResult::CHECK_FAILED;
    if (EVP_PKEY_public_check(ctx.get()) != 1) {
      return DHPointer::CheckPublicKeyResult::INVALID;
    }
    return CheckPublicKeyResult::NONE;
  }

  auto peer = NewDhPKey(p_bn, g_bn, pub_key.get());
  if (!peer) return DHPointer::CheckPublicKeyResult::CHECK_FAILED;
  EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(peer.get(), nullptr));
  if (!ctx) return DHPointer::CheckPublicKeyResult::CHECK_FAILED;
  if (EVP_PKEY_public_check(ctx.get()) != 1) {
    return DHPointer::CheckPublicKeyResult::INVALID;
  }
  return CheckPublicKeyResult::NONE;
#elif NCRYPTO_USE_BORINGSSL
  int codes = 0;
  if (DH_check_pub_key(dh_.get(), pub_key.get(), &codes) != 1) {
    return DHPointer::CheckPublicKeyResult::CHECK_FAILED;
  }
  if (codes & DH_CHECK_PUBKEY_TOO_SMALL) {
    return DHPointer::CheckPublicKeyResult::TOO_SMALL;
  } else if (codes & DH_CHECK_PUBKEY_TOO_LARGE) {
    return DHPointer::CheckPublicKeyResult::TOO_LARGE;
  }
  if (codes != 0) {
    return DHPointer::CheckPublicKeyResult::INVALID;
  }
  return CheckPublicKeyResult::NONE;
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer DHPointer::getPrime() const {
  if (!*this) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (p_) return p_.encode();

  DeleteFnPtr<BIGNUM, BN_free> p;
  DeleteFnPtr<BIGNUM, BN_free> g;
  if (!GetDhParams(dh_.get(), &p, &g)) return {};
  return BignumPointer::Encode(p.get());
#elif NCRYPTO_USE_BORINGSSL
  const BIGNUM* p;
  DH_get0_pqg(dh_.get(), &p, nullptr, nullptr);
  return BignumPointer::Encode(p);
#else
#error "Unsupported crypto backend"
#endif
}

size_t DHPointer::getPrimeBits() const {
  if (!*this) return 0;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (p_) return BignumPointer::GetBitCount(p_.get());

  DeleteFnPtr<BIGNUM, BN_free> p;
  DeleteFnPtr<BIGNUM, BN_free> g;
  if (!GetDhParams(dh_.get(), &p, &g)) return 0;
  return BignumPointer::GetBitCount(p.get());
#elif NCRYPTO_USE_BORINGSSL
  const BIGNUM* p;
  DH_get0_pqg(dh_.get(), &p, nullptr, nullptr);
  return BignumPointer::GetBitCount(p);
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer DHPointer::getGenerator() const {
  if (!*this) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (g_) return g_.encode();

  DeleteFnPtr<BIGNUM, BN_free> p;
  DeleteFnPtr<BIGNUM, BN_free> g;
  if (!GetDhParams(dh_.get(), &p, &g)) return {};
  return BignumPointer::Encode(g.get());
#elif NCRYPTO_USE_BORINGSSL
  const BIGNUM* g;
  DH_get0_pqg(dh_.get(), nullptr, nullptr, &g);
  return BignumPointer::Encode(g);
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer DHPointer::getPublicKey() const {
  if (!*this) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (pub_key_) return pub_key_.encode();
  if (!dh_) return {};

  DeleteFnPtr<BIGNUM, BN_free> pub_key;
  if (!GetDhKeys(dh_.get(), &pub_key, nullptr)) return {};
  return BignumPointer::Encode(pub_key.get());
#elif NCRYPTO_USE_BORINGSSL
  const BIGNUM* pub_key;
  DH_get0_key(dh_.get(), &pub_key, nullptr);
  return BignumPointer::Encode(pub_key);
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer DHPointer::getPrivateKey() const {
  if (!*this) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (pvt_key_) return pvt_key_.encode();
  if (!dh_) return {};

  DeleteFnPtr<BIGNUM, BN_clear_free> pvt_key;
  if (!GetDhKeys(dh_.get(), nullptr, &pvt_key)) return {};
  return BignumPointer::Encode(pvt_key.get());
#elif NCRYPTO_USE_BORINGSSL
  const BIGNUM* pvt_key;
  DH_get0_key(dh_.get(), nullptr, &pvt_key);
  return BignumPointer::Encode(pvt_key);
#else
#error "Unsupported crypto backend"
#endif
}

bool DHPointer::hasPrivateKey() const {
  if (!*this) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (pvt_key_) return true;
  if (!dh_) return false;

  DeleteFnPtr<BIGNUM, BN_clear_free> pvt_key;
  if (!GetDhKeys(dh_.get(), nullptr, &pvt_key)) return false;
  return pvt_key != nullptr;
#elif NCRYPTO_USE_BORINGSSL
  const BIGNUM* pvt_key = nullptr;
  DH_get0_key(dh_.get(), nullptr, &pvt_key);
  return pvt_key != nullptr;
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer DHPointer::generateKeys() {
  ClearErrorOnReturn clearErrorOnReturn;
  if (!*this) return {};

#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (p_ && g_) {
    if (!pvt_key_ && !GenerateDhPrivateKey(&pvt_key_, p_.get(), group_name_)) {
      return {};
    }

    BignumPointer generated_pub_key;
    if (!GenerateDhPublicKey(
            &generated_pub_key, p_.get(), g_.get(), pvt_key_.get())) {
      return {};
    }

    if (pub_key_ && BN_cmp(pub_key_.get(), generated_pub_key.get()) == 0) {
      return getPublicKey();
    }

    pub_key_ = std::move(generated_pub_key);
    return getPublicKey();
  }

  DeleteFnPtr<BIGNUM, BN_free> p;
  DeleteFnPtr<BIGNUM, BN_free> g;
  DeleteFnPtr<BIGNUM, BN_free> pub_key;
  DeleteFnPtr<BIGNUM, BN_clear_free> pvt_key;
  if (!GetDhParams(dh_.get(), &p, &g) ||
      !GetDhKeys(dh_.get(), &pub_key, &pvt_key)) {
    return {};
  }

  if (pvt_key != nullptr) {
    BignumPointer generated_pub_key;
    if (!GenerateDhPublicKey(
            &generated_pub_key, p.get(), g.get(), pvt_key.get())) {
      return {};
    }

    if (pub_key != nullptr &&
        BN_cmp(pub_key.get(), generated_pub_key.get()) == 0) {
      return getPublicKey();
    }

    auto replacement =
        group_name_ != nullptr
            ? NewDhPKey(group_name_, generated_pub_key.get(), pvt_key.get())
            : NewDhPKey(
                  p.get(), g.get(), generated_pub_key.get(), pvt_key.get());
    if (!replacement) return {};
    dh_.reset(replacement.release());
    return getPublicKey();
  }

  EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(dh_.get(), nullptr));
  if (!ctx || !ctx.initForKeygen()) return {};
  EVP_PKEY* generated = nullptr;
  if (EVP_PKEY_keygen(ctx.get(), &generated) != 1) return {};
  dh_.reset(generated);
  return getPublicKey();
#elif NCRYPTO_USE_BORINGSSL
  // Key generation failed
  if (!DH_generate_key(dh_.get())) return {};

  return getPublicKey();
#else
#error "Unsupported crypto backend"
#endif
}

size_t DHPointer::size() const {
  if (!*this) return 0;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (p_) return BignumPointer::GetByteCount(p_.get());

  const int bits = EVP_PKEY_get_bits(dh_.get());
  return bits > 0 ? (static_cast<size_t>(bits) + 7) / 8 : 0;
#elif NCRYPTO_USE_BORINGSSL
  int ret = DH_size(dh_.get());
  // DH_size can return a -1 on error but we just want to return a 0
  // in that case so we don't wrap around when returning the size_t.
  return ret >= 0 ? static_cast<size_t>(ret) : 0;
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer DHPointer::computeSecret(const BignumPointer& peer) const {
  ClearErrorOnReturn clearErrorOnReturn;
  if (!*this || !peer) return {};

#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (p_ && pvt_key_) {
    auto secret = BignumPointer::NewSecure();
    BignumCtxPointer ctx(BN_CTX_new());
    if (!secret || !ctx ||
        BN_mod_exp_mont_consttime(secret.get(),
                                  peer.get(),
                                  pvt_key_.get(),
                                  p_.get(),
                                  ctx.get(),
                                  nullptr) != 1) {
      return {};
    }
    return secret.encodePadded(size());
  }

  EVPKeyPointer peer_key;
  if (group_name_ != nullptr) {
    peer_key = NewDhPKey(group_name_, peer.get());
  } else {
    DeleteFnPtr<BIGNUM, BN_free> p;
    DeleteFnPtr<BIGNUM, BN_free> g;
    if (!GetDhParams(dh_.get(), &p, &g)) return {};
    peer_key = NewDhPKey(p.get(), g.get(), peer.get());
  }
  if (!peer_key) return {};

  EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(dh_.get(), nullptr));
  size_t out_size = size();
  if (!ctx || EVP_PKEY_derive_init(ctx.get()) != 1 ||
      EVP_PKEY_CTX_set_dh_pad(ctx.get(), 1) != 1 ||
      EVP_PKEY_derive_set_peer(ctx.get(), peer_key.get()) != 1 ||
      EVP_PKEY_derive(ctx.get(), nullptr, &out_size) != 1) {
    return {};
  }

  if (out_size == 0) return {};

  auto dp = DataPointer::Alloc(out_size);
  if (!dp) return {};
  if (EVP_PKEY_derive(
          ctx.get(), static_cast<unsigned char*>(dp.get()), &out_size) != 1) {
    return {};
  }
  return dp.resize(out_size);
#elif NCRYPTO_USE_BORINGSSL
  auto dp = DataPointer::Alloc(size());
  if (!dp) return {};

  int size =
      DH_compute_key(static_cast<uint8_t*>(dp.get()), peer.get(), dh_.get());
  if (size < 0) return {};

  // The size of the computed key can be smaller than the size of the DH key.
  // We want to make sure that the key is correctly padded.
  if (static_cast<size_t>(size) < dp.size()) {
    const size_t padding = dp.size() - size;
    uint8_t* data = static_cast<uint8_t*>(dp.get());
    memmove(data + padding, data, size);
    memset(data, 0, padding);
  }

  return dp;
#else
#error "Unsupported crypto backend"
#endif
}

bool DHPointer::setPublicKey(BignumPointer&& key) {
  if (!*this) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (p_ && g_) {
    pub_key_ = std::move(key);
    return true;
  }

  DeleteFnPtr<BIGNUM, BN_clear_free> pvt_key;
  if (!GetDhKeys(dh_.get(), nullptr, &pvt_key)) {
    return false;
  }
  EVPKeyPointer pkey;
  if (group_name_ != nullptr) {
    pkey = NewDhPKey(group_name_, key.get(), pvt_key.get());
  } else {
    DeleteFnPtr<BIGNUM, BN_free> p;
    DeleteFnPtr<BIGNUM, BN_free> g;
    if (!GetDhParams(dh_.get(), &p, &g)) return false;
    pkey = NewDhPKey(p.get(), g.get(), key.get(), pvt_key.get());
  }
  if (!pkey) return false;
  dh_.reset(pkey.release());
  return true;
#elif NCRYPTO_USE_BORINGSSL
  if (DH_set0_key(dh_.get(), key.get(), nullptr) == 1) {
    // If DH_set0_key returns successfully, then dh_ takes ownership of the
    // BIGNUM, so we must release it here. Unfortunately coverity does not
    // know that so we need to tell it not to complain.
    // coverity[resource_leak]
    key.release();
    return true;
  }
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

bool DHPointer::setPrivateKey(BignumPointer&& key) {
  if (!*this) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (p_ && g_) {
    pvt_key_ = std::move(key);
    return true;
  }

  DeleteFnPtr<BIGNUM, BN_free> pub_key;
  if (!GetDhKeys(dh_.get(), &pub_key, nullptr)) {
    return false;
  }
  EVPKeyPointer pkey;
  if (group_name_ != nullptr) {
    pkey = NewDhPKey(group_name_, pub_key.get(), key.get());
  } else {
    DeleteFnPtr<BIGNUM, BN_free> p;
    DeleteFnPtr<BIGNUM, BN_free> g;
    if (!GetDhParams(dh_.get(), &p, &g)) return false;
    pkey = NewDhPKey(p.get(), g.get(), pub_key.get(), key.get());
  }
  if (!pkey) return false;
  dh_.reset(pkey.release());
  return true;
#elif NCRYPTO_USE_BORINGSSL
  if (DH_set0_key(dh_.get(), nullptr, key.get()) == 1) {
    // If DH_set0_key returns successfully, then dh_ takes ownership of the
    // BIGNUM, so we must release it here. Unfortunately coverity does not
    // know that so we need to tell it not to complain.
    // coverity[resource_leak]
    key.release();
    return true;
  }
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey,
                                 const EVPKeyPointer& theirKey) {
  size_t out_size;
  if (!ourKey || !theirKey) return {};

  auto ctx = EVPKeyCtxPointer::New(ourKey);
  if (!ctx || EVP_PKEY_derive_init(ctx.get()) <= 0) {
    return {};
  }
  // TODO(panva): In a semver-major, consider padding OpenSSL DH derivation
  // results here to match DiffieHellman::computeSecret().
  if (EVP_PKEY_derive_set_peer(ctx.get(), theirKey.get()) <= 0 ||
      EVP_PKEY_derive(ctx.get(), nullptr, &out_size) <= 0) {
    return {};
  }

  if (out_size == 0) return {};

  auto out = DataPointer::Alloc(out_size);
  if (EVP_PKEY_derive(
          ctx.get(), reinterpret_cast<uint8_t*>(out.get()), &out_size) <= 0) {
    return {};
  }

  if (out_size < out.size()) {
    const size_t padding = out.size() - out_size;
    uint8_t* data = static_cast<uint8_t*>(out.get());
    memmove(data + padding, data, out_size);
    memset(data, 0, padding);
  }

  return out;
}

// ============================================================================
// KDF

#if NCRYPTO_USE_OPENSSL_PROVIDER
KDF::KDF(EVP_KDF* kdf) : kdf_(kdf) {}

KDF KDF::Fetch(const char* algorithm, OSSL_LIB_CTX* libctx) {
  return KDF(EVP_KDF_fetch(libctx, algorithm, nullptr));
}

bool KDF::derive(const Buffer<unsigned char>& out,
                 const OSSL_PARAM* params) const {
  if (!kdf_) return false;
  DeleteFnPtr<EVP_KDF_CTX, EVP_KDF_CTX_free> ctx(EVP_KDF_CTX_new(kdf_.get()));
  return ctx && EVP_KDF_derive(ctx.get(), out.data, out.len, params) == 1;
}
#endif

const EVP_MD* getDigestByName(const char* name) {
#if NCRYPTO_USE_BORINGSSL
  // BoringSSL exposes SHA-3 through EVP_MD descriptors without registering
  // their names or assigning them NIDs.
  if (CaseInsensitiveNameEqual()(name, "sha3-256")) return EVP_sha3_256();
  if (CaseInsensitiveNameEqual()(name, "sha3-384")) return EVP_sha3_384();
  if (CaseInsensitiveNameEqual()(name, "sha3-512")) return EVP_sha3_512();
#endif
  return EVP_get_digestbyname(name);
}

bool checkHkdfLength(const Digest& md, size_t length) {
  // HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as
  // the output of the hash function. 255 is a hard limit because HKDF appends
  // an 8-bit counter to each HMAC'd message, starting at 1.
  static constexpr size_t kMaxDigestMultiplier = 255;
  size_t max_length = md.size() * kMaxDigestMultiplier;
  if (length > max_length) return false;
  return true;
}

DataPointer hkdf(const Digest& md,
                 const Buffer<const unsigned char>& key,
                 const Buffer<const unsigned char>& info,
                 const Buffer<const unsigned char>& salt,
                 size_t length) {
  if (!checkHkdfLength(md, length) || info.len > INT_MAX ||
      salt.len > INT_MAX) {
    return {};
  }

  std::string_view actual_salt;
  static const char default_salt[EVP_MAX_MD_SIZE] = {0};
  if (salt.len > 0) {
    actual_salt = {reinterpret_cast<const char*>(salt.data), salt.len};
  } else {
    actual_salt = {default_salt, static_cast<unsigned>(md.size())};
  }

  // Keep extraction as a one-shot HMAC because BoringSSL's EVP_PKEY_derive
  // rejects the zero-length keys Web Crypto allows. Both backends expand a
  // pseudorandom key of exactly one digest block.
  unsigned char pseudorandom_key[EVP_MAX_MD_SIZE];
  unsigned pseudorandom_key_len = sizeof(pseudorandom_key);

  if (HMAC(md,
           actual_salt.data(),
           actual_salt.size(),
           key.data,
           key.len,
           pseudorandom_key,
           &pseudorandom_key_len) == nullptr) {
    return {};
  }

  auto buf = DataPointer::Alloc(length);
  if (!buf) return {};

#if NCRYPTO_USE_OPENSSL_PROVIDER
  // Expand through EVP_KDF directly. The EVP_PKEY_HKDF interface reaches the
  // same provider implementation, but only after allocating a second context
  // and translating every parameter across the legacy bridge.
  auto kdf = KDF::Fetch(OSSL_KDF_NAME_HKDF);
  if (!kdf) return {};

  const char* md_name = EVP_MD_get0_name(md);
  if (md_name == nullptr) return {};

  int mode = EVP_KDF_HKDF_MODE_EXPAND_ONLY;
  std::array<OSSL_PARAM, 5> params;
  size_t n = 0;
  params[n++] = OSSL_PARAM_construct_int(OSSL_KDF_PARAM_MODE, &mode);
  params[n++] = OSSL_PARAM_construct_utf8_string(
      OSSL_KDF_PARAM_DIGEST, const_cast<char*>(md_name), 0);
  params[n++] = OSSL_PARAM_construct_octet_string(
      OSSL_KDF_PARAM_KEY, pseudorandom_key, pseudorandom_key_len);
  if (info.len > 0) {
    params[n++] = OSSL_PARAM_construct_octet_string(
        OSSL_KDF_PARAM_INFO, const_cast<unsigned char*>(info.data), info.len);
  }
  params[n++] = OSSL_PARAM_construct_end();

  if (!kdf.derive({buf.get<unsigned char>(), length}, params.data())) {
    return {};
  }
#elif NCRYPTO_USE_BORINGSSL
  auto ctx = EVPKeyCtxPointer::NewFromName("HKDF");
  if (!ctx || !EVP_PKEY_derive_init(ctx.get()) ||
      !EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) ||
      !EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len) ||
      !EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) ||
      !EVP_PKEY_CTX_set1_hkdf_key(
          ctx.get(), pseudorandom_key, pseudorandom_key_len)) {
    return {};
  }

  if (EVP_PKEY_derive(
          ctx.get(), static_cast<unsigned char*>(buf.get()), &length) <= 0) {
    return {};
  }
#else
#error "Unsupported crypto backend"
#endif

  return buf;
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
namespace {
bool ScryptDerive(const Buffer<const char>& pass,
                  const Buffer<const unsigned char>& salt,
                  uint64_t N,
                  uint64_t r,
                  uint64_t p,
                  uint64_t maxmem,
                  unsigned char* out,
                  size_t length) {
  // EVP_PBE_scrypt limits these parameters to the provider's declared width.
  if (r > UINT32_MAX || p > UINT32_MAX) {
    ERR_raise(ERR_LIB_EVP, EVP_R_PARAMETER_TOO_LARGE);
    return false;
  }

  // Keep EVP_PBE_scrypt's 32 MiB default instead of the provider's default.
  if (maxmem == 0) maxmem = 32 * 1024 * 1024;

  auto kdf = KDF::Fetch(OSSL_KDF_NAME_SCRYPT);
  if (!kdf) return false;

  unsigned char empty_salt = 0;
  OSSL_PARAM params[] = {
      OSSL_PARAM_construct_octet_string(
          OSSL_KDF_PARAM_PASSWORD,
          const_cast<char*>(pass.data == nullptr ? "" : pass.data),
          pass.data == nullptr ? 0 : pass.len),
      OSSL_PARAM_construct_octet_string(
          OSSL_KDF_PARAM_SALT,
          salt.data == nullptr ? &empty_salt
                               : const_cast<unsigned char*>(salt.data),
          salt.data == nullptr ? 0 : salt.len),
      OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_N, &N),
      OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_R, &r),
      OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_P, &p),
      OSSL_PARAM_construct_uint64(OSSL_KDF_PARAM_SCRYPT_MAXMEM, &maxmem),
      OSSL_PARAM_END,
  };
  return kdf.derive({out, length}, params);
}
}  // namespace
#endif

bool checkScryptParams(uint64_t N, uint64_t r, uint64_t p, uint64_t maxmem) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  // A null output validates the parameters without deriving a key.
  return ScryptDerive({nullptr, 0}, {nullptr, 0}, N, r, p, maxmem, nullptr, 0);
#elif NCRYPTO_USE_BORINGSSL
  return EVP_PBE_scrypt(nullptr, 0, nullptr, 0, N, r, p, maxmem, nullptr, 0) ==
         1;
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer scrypt(const Buffer<const char>& pass,
                   const Buffer<const unsigned char>& salt,
                   uint64_t N,
                   uint64_t r,
                   uint64_t p,
                   uint64_t maxmem,
                   size_t length) {
  if (pass.len > INT_MAX || salt.len > INT_MAX) {
    return {};
  }

  auto dp = DataPointer::Alloc(length);
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (dp && ScryptDerive(
                pass, salt, N, r, p, maxmem, dp.get<unsigned char>(), length)) {
#elif NCRYPTO_USE_BORINGSSL
  if (dp && EVP_PBE_scrypt(pass.data,
                           pass.len,
                           salt.data,
                           salt.len,
                           N,
                           r,
                           p,
                           maxmem,
                           reinterpret_cast<unsigned char*>(dp.get()),
                           length)) {
#else
#error "Unsupported crypto backend"
#endif
    return dp;
  }

  return {};
}

DataPointer pbkdf2(const Digest& md,
                   const Buffer<const char>& pass,
                   const Buffer<const unsigned char>& salt,
                   uint32_t iterations,
                   size_t length) {
  if (pass.len > INT_MAX || salt.len > INT_MAX || length > INT_MAX) {
    return {};
  }

  auto dp = DataPointer::Alloc(length);
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!dp) return {};
  auto kdf = KDF::Fetch(OSSL_KDF_NAME_PBKDF2);
  if (!kdf) return {};

  const char* md_name = EVP_MD_get0_name(md);
  if (md_name == nullptr) return {};

#if OPENSSL_VERSION_MAJOR < 4
  // Match PKCS5_PBKDF2_HMAC: OpenSSL 3 disables the provider's lower bounds,
  // while OpenSSL 4 leaves them at their provider defaults.
  int pkcs5 = 1;
#endif
  int iteration_count = static_cast<int>(iterations);
  unsigned char empty_salt = 0;
  OSSL_PARAM params[] = {
    OSSL_PARAM_construct_octet_string(
        OSSL_KDF_PARAM_PASSWORD,
        const_cast<char*>(pass.data == nullptr ? "" : pass.data),
        pass.data == nullptr ? 0 : pass.len),
#if OPENSSL_VERSION_MAJOR < 4
    OSSL_PARAM_construct_int(OSSL_KDF_PARAM_PKCS5, &pkcs5),
#endif
    OSSL_PARAM_construct_octet_string(
        OSSL_KDF_PARAM_SALT,
        salt.data == nullptr && salt.len == 0
            ? &empty_salt
            : const_cast<unsigned char*>(salt.data),
        salt.len),
    OSSL_PARAM_construct_int(OSSL_KDF_PARAM_ITER, &iteration_count),
    OSSL_PARAM_construct_utf8_string(
        OSSL_KDF_PARAM_DIGEST, const_cast<char*>(md_name), 0),
    OSSL_PARAM_END,
  };
  if (kdf.derive({dp.get<unsigned char>(), length}, params)) {
    return dp;
  }
#elif NCRYPTO_USE_BORINGSSL
  const EVP_MD* md_ptr = md;
  if (dp && PKCS5_PBKDF2_HMAC(pass.data,
                              pass.len,
                              salt.data,
                              salt.len,
                              iterations,
                              md_ptr,
                              length,
                              reinterpret_cast<unsigned char*>(dp.get()))) {
    return dp;
  }
#else
#error "Unsupported crypto backend"
#endif

  return {};
}

#if OPENSSL_WITH_ARGON2
DataPointer argon2(const Buffer<const char>& pass,
                   const Buffer<const unsigned char>& salt,
                   uint32_t lanes,
                   size_t length,
                   uint32_t memcost,
                   uint32_t iter,
                   uint32_t version,
                   const Buffer<const unsigned char>& secret,
                   const Buffer<const unsigned char>& ad,
                   Argon2Type type) {
  std::string_view algorithm;
  switch (type) {
    case Argon2Type::ARGON2I:
      algorithm = "ARGON2I";
      break;
    case Argon2Type::ARGON2D:
      algorithm = "ARGON2D";
      break;
    case Argon2Type::ARGON2ID:
      algorithm = "ARGON2ID";
      break;
    default:
      // Invalid Argon2 type
      return {};
  }

  // A new library context is only needed for OSSL_set_max_threads(), which is
  // per-context. It inherits no configuration, so availability is checked
  // against the default context, otherwise Argon2 works in FIPS mode.
  DeleteFnPtr<OSSL_LIB_CTX, OSSL_LIB_CTX_free> ctx;
  uint32_t threads = lanes == 0 ? 0 : 1;
  if (lanes > 1 && (OSSL_get_thread_support_flags() &
                    OSSL_THREAD_SUPPORT_FLAG_DEFAULT_SPAWN) != 0) {
    if (!KDF::Fetch(algorithm.data())) {
      return {};
    }

    ctx.reset(OSSL_LIB_CTX_new());
    if (!ctx) {
      return {};
    }

    MarkPopErrorOnReturn mark_pop_error_on_return;
    if (OSSL_set_max_threads(ctx.get(), lanes) == 1) {
      threads = lanes;
    } else {
      // Lane count is an Argon2 input; worker threads are only an
      // optimization. Compute the same lanes serially if unavailable.
      ctx.reset();
    }
  }

  auto kdf = KDF::Fetch(algorithm.data(), ctx.get());
  if (!kdf) {
    return {};
  }

  std::vector<OSSL_PARAM> params;
  params.reserve(9);

  params.push_back(OSSL_PARAM_construct_octet_string(
      OSSL_KDF_PARAM_PASSWORD,
      const_cast<char*>(pass.len > 0 ? pass.data : ""),
      pass.len));
  params.push_back(OSSL_PARAM_construct_octet_string(
      OSSL_KDF_PARAM_SALT, const_cast<unsigned char*>(salt.data), salt.len));
  params.push_back(
      OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_THREADS, &threads));
  params.push_back(
      OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_LANES, &lanes));
  params.push_back(
      OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ARGON2_MEMCOST, &memcost));
  params.push_back(OSSL_PARAM_construct_uint32(OSSL_KDF_PARAM_ITER, &iter));

  if (ad.len != 0) {
    params.push_back(OSSL_PARAM_construct_octet_string(
        OSSL_KDF_PARAM_ARGON2_AD, const_cast<unsigned char*>(ad.data), ad.len));
  }

  if (secret.len != 0) {
    params.push_back(OSSL_PARAM_construct_octet_string(
        OSSL_KDF_PARAM_SECRET,
        const_cast<unsigned char*>(secret.data),
        secret.len));
  }

  params.push_back(OSSL_PARAM_construct_end());

  auto dp = DataPointer::Alloc(length);
  if (dp && kdf.derive({dp.get<unsigned char>(), length}, params.data())) {
    return dp;
  }

  return {};
}
#endif

// ============================================================================

EVPKeyPointer::PrivateKeyEncodingConfig::PrivateKeyEncodingConfig(
    const PrivateKeyEncodingConfig& other)
    : PrivateKeyEncodingConfig(
          other.output_key_object, other.format, other.type) {
  cipher = other.cipher;
  if (other.passphrase.has_value()) {
    auto& otherPassphrase = other.passphrase.value();
    auto newPassphrase = DataPointer::Alloc(otherPassphrase.size());
    memcpy(newPassphrase.get(), otherPassphrase.get(), otherPassphrase.size());
    passphrase = std::move(newPassphrase);
  }
}

EVPKeyPointer::AsymmetricKeyEncodingConfig::AsymmetricKeyEncodingConfig(
    bool output_key_object, PKFormatType format, PKEncodingType type)
    : output_key_object(output_key_object), format(format), type(type) {}

EVPKeyPointer::PrivateKeyEncodingConfig&
EVPKeyPointer::PrivateKeyEncodingConfig::operator=(
    const PrivateKeyEncodingConfig& other) {
  if (this == &other) return *this;
  this->~PrivateKeyEncodingConfig();
  return *new (this) PrivateKeyEncodingConfig(other);
}

// clang-format off
// NOLINTBEGIN(whitespace/line_length)
const KeyAlgorithm KeyAlgorithm::RSA("RSA", Family::Other);
const KeyAlgorithm KeyAlgorithm::RSA_PSS("RSA-PSS", Family::Other);
const KeyAlgorithm KeyAlgorithm::DSA("DSA", Family::Other);
const KeyAlgorithm KeyAlgorithm::DH("DH", Family::Other);
const KeyAlgorithm KeyAlgorithm::EC("EC", Family::Other);
const KeyAlgorithm KeyAlgorithm::ED25519("Ed25519", Family::EdDSA);
const KeyAlgorithm KeyAlgorithm::ED448("Ed448", Family::EdDSA);
const KeyAlgorithm KeyAlgorithm::X25519("X25519", Family::XDH);
const KeyAlgorithm KeyAlgorithm::X448("X448", Family::XDH);
const KeyAlgorithm KeyAlgorithm::SM2("SM2", Family::Other, /* has_key_type */ false);
const KeyAlgorithm KeyAlgorithm::ML_DSA_44("ML-DSA-44", Family::MLDSA);
const KeyAlgorithm KeyAlgorithm::ML_DSA_65("ML-DSA-65", Family::MLDSA);
const KeyAlgorithm KeyAlgorithm::ML_DSA_87("ML-DSA-87", Family::MLDSA);
const KeyAlgorithm KeyAlgorithm::ML_KEM_512("ML-KEM-512", Family::MLKEM);
const KeyAlgorithm KeyAlgorithm::ML_KEM_768("ML-KEM-768", Family::MLKEM);
const KeyAlgorithm KeyAlgorithm::ML_KEM_1024("ML-KEM-1024", Family::MLKEM);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_128F("SLH-DSA-SHA2-128f", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_128S("SLH-DSA-SHA2-128s", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_192F("SLH-DSA-SHA2-192f", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_192S("SLH-DSA-SHA2-192s", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_256F("SLH-DSA-SHA2-256f", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHA2_256S("SLH-DSA-SHA2-256s", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_128F("SLH-DSA-SHAKE-128f", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_128S("SLH-DSA-SHAKE-128s", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_192F("SLH-DSA-SHAKE-192f", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_192S("SLH-DSA-SHAKE-192s", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_256F("SLH-DSA-SHAKE-256f", Family::SLHDSA);
const KeyAlgorithm KeyAlgorithm::SLH_DSA_SHAKE_256S("SLH-DSA-SHAKE-256s", Family::SLHDSA);
// NOLINTEND(whitespace/line_length)
// clang-format on

namespace {
#if NCRYPTO_USE_OPENSSL_PROVIDER
constexpr char kSignatureContextString[] = "context-string";
constexpr char kSignatureInstance[] = "instance";
#endif
const KeyAlgorithm* const kKeyAlgorithms[] = {
    &KeyAlgorithm::RSA,
    &KeyAlgorithm::RSA_PSS,
    &KeyAlgorithm::DSA,
    &KeyAlgorithm::DH,
    &KeyAlgorithm::EC,
    &KeyAlgorithm::ED25519,
    &KeyAlgorithm::ED448,
    &KeyAlgorithm::X25519,
    &KeyAlgorithm::X448,
    &KeyAlgorithm::SM2,
    &KeyAlgorithm::ML_DSA_44,
    &KeyAlgorithm::ML_DSA_65,
    &KeyAlgorithm::ML_DSA_87,
    &KeyAlgorithm::ML_KEM_512,
    &KeyAlgorithm::ML_KEM_768,
    &KeyAlgorithm::ML_KEM_1024,
    &KeyAlgorithm::SLH_DSA_SHA2_128F,
    &KeyAlgorithm::SLH_DSA_SHA2_128S,
    &KeyAlgorithm::SLH_DSA_SHA2_192F,
    &KeyAlgorithm::SLH_DSA_SHA2_192S,
    &KeyAlgorithm::SLH_DSA_SHA2_256F,
    &KeyAlgorithm::SLH_DSA_SHA2_256S,
    &KeyAlgorithm::SLH_DSA_SHAKE_128F,
    &KeyAlgorithm::SLH_DSA_SHAKE_128S,
    &KeyAlgorithm::SLH_DSA_SHAKE_192F,
    &KeyAlgorithm::SLH_DSA_SHAKE_192S,
    &KeyAlgorithm::SLH_DSA_SHAKE_256F,
    &KeyAlgorithm::SLH_DSA_SHAKE_256S,
};
}  // namespace

const KeyAlgorithm* KeyAlgorithm::FromName(const char* name) {
  if (name == nullptr) return nullptr;
  for (const auto* algorithm : kKeyAlgorithms) {
    if (CaseInsensitiveNameEqual()(name, algorithm->name())) return algorithm;
  }
  return nullptr;
}

bool KeyAlgorithm::isRsa() const {
  return this == &RSA || this == &RSA_PSS;
}

bool KeyAlgorithm::isPqc() const {
  return family_ == Family::MLDSA || family_ == Family::MLKEM ||
         family_ == Family::SLHDSA;
}

bool KeyAlgorithm::isOkp() const {
  return family_ == Family::EdDSA || family_ == Family::XDH;
}

bool KeyAlgorithm::isOneShot() const {
  return family_ == Family::EdDSA || family_ == Family::MLDSA ||
         family_ == Family::SLHDSA;
}

bool KeyAlgorithm::supportsRawPublic() const {
  return isOkp() || isPqc();
}

bool KeyAlgorithm::supportsRawPrivate() const {
  return isOkp() || family_ == Family::SLHDSA;
}

size_t KeyAlgorithm::seedSize() const {
  if (family_ == Family::MLDSA) return 32;
  if (family_ == Family::MLKEM) return 64;
  return 0;
}

namespace {
#if NCRYPTO_USE_BORINGSSL
struct BoringSSLKeyAlgorithm {
  const char* name;
  int id;
  const EVP_PKEY_ALG* (*raw_key_algorithm)() = nullptr;
};

// BoringSSL requires native key IDs and EVP_PKEY_ALG descriptors for raw keys;
// keep both adapters in the same table.
// clang-format off
// NOLINTBEGIN(whitespace/line_length)
const BoringSSLKeyAlgorithm kBoringSSLKeyAlgorithms[] = {
    {KeyAlgorithm::RSA.name(), EVP_PKEY_RSA},
    {KeyAlgorithm::RSA_PSS.name(), EVP_PKEY_RSA_PSS},
    {KeyAlgorithm::DSA.name(), EVP_PKEY_DSA},
    {KeyAlgorithm::DH.name(), EVP_PKEY_DH},
    {KeyAlgorithm::EC.name(), EVP_PKEY_EC},
    {KeyAlgorithm::ED25519.name(), EVP_PKEY_ED25519, EVP_pkey_ed25519},
    {KeyAlgorithm::X25519.name(), EVP_PKEY_X25519, EVP_pkey_x25519},
    {"HKDF", EVP_PKEY_HKDF},
    {KeyAlgorithm::ED448.name(), EVP_PKEY_ED448},
    {KeyAlgorithm::X448.name(), EVP_PKEY_X448},
    {KeyAlgorithm::ML_DSA_44.name(), EVP_PKEY_ML_DSA_44, EVP_pkey_ml_dsa_44},
    {KeyAlgorithm::ML_DSA_65.name(), EVP_PKEY_ML_DSA_65, EVP_pkey_ml_dsa_65},
    {KeyAlgorithm::ML_DSA_87.name(), EVP_PKEY_ML_DSA_87, EVP_pkey_ml_dsa_87},
    {KeyAlgorithm::ML_KEM_768.name(), EVP_PKEY_ML_KEM_768, EVP_pkey_ml_kem_768},
    {KeyAlgorithm::ML_KEM_1024.name(), EVP_PKEY_ML_KEM_1024, EVP_pkey_ml_kem_1024},
};
// NOLINTEND(whitespace/line_length)
// clang-format on

const BoringSSLKeyAlgorithm* FindBoringSSLKeyAlgorithm(const char* name) {
  if (name == nullptr) return nullptr;
  for (const auto& algorithm : kBoringSSLKeyAlgorithms) {
    if (CaseInsensitiveNameEqual()(name, algorithm.name)) return &algorithm;
  }
  return nullptr;
}

int GetBoringSSLKeyId(const char* name) {
  const auto* algorithm = FindBoringSSLKeyAlgorithm(name);
  return algorithm == nullptr ? NID_undef : algorithm->id;
}

const EVP_PKEY_ALG* GetBoringSSLKeyAlgorithm(const KeyAlgorithm& algorithm) {
  const auto* entry = FindBoringSSLKeyAlgorithm(algorithm.name());
  return entry != nullptr && entry->raw_key_algorithm != nullptr
             ? entry->raw_key_algorithm()
             : nullptr;
}
#endif
}  // namespace

void ConfigurePqcEncoding() {
#if NCRYPTO_USE_OPENSSL_PROVIDER && OPENSSL_VERSION_PREREQ(3, 5)
  // Configure all loaded providers to prefer seed-only format for ML-KEM and
  // ML-DSA private keys in PKCS#8 export, falling back to priv-only when a
  // seed is not available. The provider encoder reads these parameters at
  // encoding time via ossl_prov_ctx_get_param().
  OSSL_PROVIDER_do_all(
      nullptr,
      [](OSSL_PROVIDER* provider, void*) -> int {
        OSSL_PROVIDER_add_conf_parameter(
            provider, "ml-kem.output_formats", "seed-only,priv-only");
        OSSL_PROVIDER_add_conf_parameter(
            provider, "ml-dsa.output_formats", "seed-only,priv-only");
        return 1;
      },
      nullptr);
#endif
}

bool KeyAlgorithm::isAvailable() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  return static_cast<bool>(EVPKeyCtxPointer::NewFromName(name_));
}

EVPKeyPointer EVPKeyPointer::New() {
  return EVPKeyPointer(EVP_PKEY_new());
}

EVPKeyPointer EVPKeyPointer::NewRawPublic(
    const KeyAlgorithm& algorithm, const Buffer<const unsigned char>& data) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return EVPKeyPointer(EVP_PKEY_new_raw_public_key_ex(
      nullptr, algorithm.name(), nullptr, data.data, data.len));
#elif NCRYPTO_USE_BORINGSSL
  const auto* alg = GetBoringSSLKeyAlgorithm(algorithm);
  if (alg == nullptr) return {};
  return EVPKeyPointer(EVP_PKEY_from_raw_public_key(alg, data.data, data.len));
#else
#error "Unsupported crypto backend"
#endif
}

EVPKeyPointer EVPKeyPointer::NewRawPrivate(
    const KeyAlgorithm& algorithm, const Buffer<const unsigned char>& data) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return EVPKeyPointer(EVP_PKEY_new_raw_private_key_ex(
      nullptr, algorithm.name(), nullptr, data.data, data.len));
#elif NCRYPTO_USE_BORINGSSL
  const auto* alg = GetBoringSSLKeyAlgorithm(algorithm);
  if (alg == nullptr) return {};
  return EVPKeyPointer(EVP_PKEY_from_raw_private_key(alg, data.data, data.len));
#else
#error "Unsupported crypto backend"
#endif
}

EVPKeyPointer EVPKeyPointer::NewRawSeed(
    const KeyAlgorithm& algorithm, const Buffer<const unsigned char>& data) {
  if (algorithm.seedSize() == 0) return {};
#if NCRYPTO_USE_BORINGSSL
  const auto* seed_alg = GetBoringSSLKeyAlgorithm(algorithm);
  if (seed_alg == nullptr) return {};
  return EVPKeyPointer(
      EVP_PKEY_from_private_seed(seed_alg, data.data, data.len));
#elif NCRYPTO_USE_OPENSSL_PROVIDER
  // ML-DSA and ML-KEM both use the provider parameter "seed".
  OSSL_PARAM params[] = {
      OSSL_PARAM_construct_octet_string(
          "seed", const_cast<unsigned char*>(data.data), data.len),
      OSSL_PARAM_END};
  auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(algorithm);
  if (!ctx) return {};
  EVP_PKEY* pkey = nullptr;
  if (EVP_PKEY_fromdata_init(ctx.get()) <= 0 ||
      EVP_PKEY_fromdata(ctx.get(), &pkey, EVP_PKEY_KEYPAIR, params) <= 0) {
    return {};
  }
  return EVPKeyPointer(pkey);
#else
#error "Unsupported crypto backend"
#endif
}

EVPKeyPointer EVPKeyPointer::NewDH(DHPointer&& dh) {
  if (!dh) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return EVPKeyPointer(dh.release());
#elif NCRYPTO_USE_BORINGSSL
  auto key = New();
  if (!key) return {};
  if (EVP_PKEY_assign_DH(key.get(), dh.get())) {
    dh.release();
  }
  return key;
#else
#error "Unsupported crypto backend"
#endif
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
EVPKeyPointer EVPKeyPointer::NewRSA(const Rsa& rsa) {
  const auto public_key = rsa.getPublicKey();
  if (public_key.n == nullptr || public_key.e == nullptr) return {};

  OSSLParamBldPointer bld(OSSL_PARAM_BLD_new());
  if (!bld ||
      OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_RSA_N, public_key.n) !=
          1 ||
      OSSL_PARAM_BLD_push_BN(bld.get(), OSSL_PKEY_PARAM_RSA_E, public_key.e) !=
          1) {
    return {};
  }

  int selection = EVP_PKEY_PUBLIC_KEY;
  if (public_key.d != nullptr) {
    const auto private_key = rsa.getPrivateKey();
    if (private_key.p == nullptr || private_key.q == nullptr ||
        private_key.dp == nullptr || private_key.dq == nullptr ||
        private_key.qi == nullptr ||
        OSSL_PARAM_BLD_push_BN(
            bld.get(), OSSL_PKEY_PARAM_RSA_D, public_key.d) != 1 ||
        OSSL_PARAM_BLD_push_BN(
            bld.get(), OSSL_PKEY_PARAM_RSA_FACTOR1, private_key.p) != 1 ||
        OSSL_PARAM_BLD_push_BN(
            bld.get(), OSSL_PKEY_PARAM_RSA_FACTOR2, private_key.q) != 1 ||
        OSSL_PARAM_BLD_push_BN(
            bld.get(), OSSL_PKEY_PARAM_RSA_EXPONENT1, private_key.dp) != 1 ||
        OSSL_PARAM_BLD_push_BN(
            bld.get(), OSSL_PKEY_PARAM_RSA_EXPONENT2, private_key.dq) != 1 ||
        OSSL_PARAM_BLD_push_BN(
            bld.get(), OSSL_PKEY_PARAM_RSA_COEFFICIENT1, private_key.qi) != 1) {
      return {};
    }

    const auto other_prime_infos = rsa.getOtherPrimeInfos();
    if (other_prime_infos.size() > kRsaOtherPrimeParamNames.size()) return {};
    for (size_t i = 0; i < other_prime_infos.size(); i++) {
      const auto& info = other_prime_infos[i];
      const auto& names = kRsaOtherPrimeParamNames[i];
      if (info.r == nullptr || info.d == nullptr || info.t == nullptr ||
          OSSL_PARAM_BLD_push_BN(bld.get(), names.factor, info.r) != 1 ||
          OSSL_PARAM_BLD_push_BN(bld.get(), names.exponent, info.d) != 1 ||
          OSSL_PARAM_BLD_push_BN(bld.get(), names.coefficient, info.t) != 1) {
        return {};
      }
    }
    selection = EVP_PKEY_KEYPAIR;
  }

  OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get()));
  if (!params) return {};
  return NewPKeyFromData(KeyAlgorithm::RSA, selection, params.get());
}
#elif NCRYPTO_USE_BORINGSSL
EVPKeyPointer EVPKeyPointer::NewRSA(RSAPointer&& rsa) {
  if (!rsa) return {};
  auto key = New();
  if (!key) return {};
  if (EVP_PKEY_assign_RSA(key.get(), rsa.get())) {
    rsa.release();
  }
  return key;
}
#else
#error "Unsupported crypto backend"
#endif  // NCRYPTO_USE_OPENSSL_PROVIDER

EVPKeyPointer::EVPKeyPointer(EVP_PKEY* pkey) : pkey_(pkey) {}

EVPKeyPointer::EVPKeyPointer(EVPKeyPointer&& other) noexcept
    : pkey_(other.release()) {}

EVPKeyPointer& EVPKeyPointer::operator=(EVPKeyPointer&& other) noexcept {
  if (this == &other) return *this;
  this->~EVPKeyPointer();
  return *new (this) EVPKeyPointer(std::move(other));
}

EVPKeyPointer::~EVPKeyPointer() {
  reset();
}

void EVPKeyPointer::reset(EVP_PKEY* pkey) {
  pkey_.reset(pkey);
}

EVP_PKEY* EVPKeyPointer::release() {
  return pkey_.release();
}

bool EVPKeyPointer::isA(const EVP_PKEY* key, const char* name) {
  if (key == nullptr || name == nullptr) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  // EVP_PKEY_is_a() can match an untyped key to an unknown legacy name.
  return EVP_PKEY_get0_type_name(key) != nullptr &&
         EVP_PKEY_is_a(key, name) == 1;
#elif NCRYPTO_USE_BORINGSSL
  const int id = GetBoringSSLKeyId(name);
  return id != NID_undef && EVP_PKEY_id(key) == id;
#else
#error "Unsupported crypto backend"
#endif
}

// Returns true unless the key is known not to be SM2, so that a key whose curve
// cannot be determined opts out of the prehashed fallback rather than into it.
bool EVPKeyPointer::mayBeSM2() const {
#if defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_NO_SM2)
  return false;
#else
  if (isA(KeyAlgorithm::SM2)) return true;
  if (!isA(KeyAlgorithm::EC)) return false;

  // An ECKeyPointer would also need the public point, which a provider-backed
  // key need not expose.
  char group_name[64];
  size_t group_name_len = 0;
  if (EVP_PKEY_get_utf8_string_param(get(),
                                     OSSL_PKEY_PARAM_GROUP_NAME,
                                     group_name,
                                     sizeof(group_name),
                                     &group_name_len) != 1) {
    return true;
  }
  return OBJ_sn2nid(group_name) == NID_sm2 ||
         EC_curve_nist2nid(group_name) == NID_sm2;
#endif
}

bool EVPKeyPointer::isA(const char* name) const {
  return isA(get(), name);
}

bool EVPKeyPointer::isA(const EVP_PKEY* key, const KeyAlgorithm& algorithm) {
  return isA(key, algorithm.name());
}

bool EVPKeyPointer::isA(const KeyAlgorithm& algorithm) const {
  return isA(get(), algorithm);
}

const KeyAlgorithm* EVPKeyPointer::getAlgorithm() const {
  if (!pkey_) return nullptr;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  // Provider primary names identify algorithms. Legacy ASN.1 methods can
  // share names (for example, SM2 uses EC), so resolve those through isA().
  // The fallback also handles providers with a noncanonical primary alias.
  if (EVP_PKEY_get0_provider(get()) != nullptr) {
    if (const auto* algorithm =
            KeyAlgorithm::FromName(EVP_PKEY_get0_type_name(get()))) {
      return algorithm;
    }
  }
  for (const auto* algorithm : kKeyAlgorithms) {
    if (isA(*algorithm)) return algorithm;
  }
#elif NCRYPTO_USE_BORINGSSL
  const int id = EVP_PKEY_id(get());
  for (const auto& algorithm : kBoringSSLKeyAlgorithms) {
    if (id == algorithm.id) return KeyAlgorithm::FromName(algorithm.name);
  }
#else
#error "Unsupported crypto backend"
#endif
  return nullptr;
}

const char* EVPKeyPointer::getKeyTypeName() const {
  const auto* algorithm = getAlgorithm();
  return algorithm == nullptr ? nullptr : algorithm->keyTypeName();
}

bool EVPKeyPointer::supportsRawPublic() const {
  const auto* algorithm = getAlgorithm();
  return algorithm != nullptr && algorithm->supportsRawPublic();
}

bool EVPKeyPointer::supportsRawPrivate() const {
  const auto* algorithm = getAlgorithm();
  return algorithm != nullptr && algorithm->supportsRawPrivate();
}

bool EVPKeyPointer::supportsContextString() const {
  const auto* algorithm = getAlgorithm();
  if (algorithm == nullptr || !algorithm->isOneShot()) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  MarkPopErrorOnReturn mark_pop_error_on_return;
  DeleteFnPtr<EVP_SIGNATURE, EVP_SIGNATURE_free> signature(
      EVP_SIGNATURE_fetch(nullptr, EVP_PKEY_get0_type_name(get()), nullptr));
  if (!signature) return false;
  const OSSL_PARAM* params = EVP_SIGNATURE_settable_ctx_params(signature.get());
  return params != nullptr &&
         OSSL_PARAM_locate_const(params, kSignatureContextString) != nullptr &&
         (algorithm != &KeyAlgorithm::ED25519 ||
          OSSL_PARAM_locate_const(params, kSignatureInstance) != nullptr);
#elif NCRYPTO_USE_BORINGSSL
  return algorithm->isPqc();
#else
#error "Unsupported crypto backend"
#endif
}

namespace {
constexpr size_t kEd25519PointSize = 32;
constexpr size_t kEd448PointSize = 57;

// Ed25519 has cofactor 8, so the first eight entries are the full
// canonical small-order subgroup: identity, one point of order 2,
// two points of order 4, and four points of order 8.
constexpr unsigned char kEd25519SmallOrderPoints[][kEd25519PointSize] = {
    // Identity.
    {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    // Order 2.
    {0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
    // Order 4.
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    // Order 8.
    {0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b,
     0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39,
     0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0x7a},
    {0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b,
     0x76, 0x0d, 0x10, 0x67, 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39,
     0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac, 0x03, 0xfa},
    {0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4,
     0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6,
     0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x05},
    {0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4,
     0x89, 0xf2, 0xef, 0x98, 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6,
     0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53, 0xfc, 0x85},
    // Non-canonical encodings of the same small-order points.
    {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
    {0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
    {0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
    {0xee, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
    {0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
    {0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f},
};

// Ed448 has cofactor 4, so these four entries are the full canonical
// small-order subgroup: identity, one point of order 2, and two points
// of order 4.
constexpr unsigned char kEd448SmallOrderPoints[][kEd448PointSize] = {
    // Identity.
    {0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    // Order 2.
    {0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
     0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00},
    // Order 4.
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80},
};

template <size_t PointSize, size_t Count>
bool ContainsPoint(const unsigned char* candidate,
                   const unsigned char (&points)[Count][PointSize]) {
  for (const auto& point : points) {
    if (memcmp(candidate, point, PointSize) == 0) return true;
  }
  return false;
}

bool IsSmallOrderEdDsaPoint(const EVPKeyPointer& key,
                            const unsigned char* candidate,
                            size_t size) {
  if (key.isA(KeyAlgorithm::ED25519)) {
    return size == kEd25519PointSize &&
           ContainsPoint(candidate, kEd25519SmallOrderPoints);
  }
  if (key.isA(KeyAlgorithm::ED448)) {
    return size == kEd448PointSize &&
           ContainsPoint(candidate, kEd448SmallOrderPoints);
  }
  return false;
}

}  // namespace

bool EVPKeyPointer::hasSmallOrderEdDsaPoint(
    const Buffer<const unsigned char>& signature) const {
  const size_t point_size = isA(KeyAlgorithm::ED25519) ? kEd25519PointSize
                            : isA(KeyAlgorithm::ED448) ? kEd448PointSize
                                                       : 0;
  if (point_size == 0) return false;

  if (signature.len != point_size * 2) return false;

  if (IsSmallOrderEdDsaPoint(*this, signature.data, point_size)) {
    return true;
  }

  unsigned char raw_public_key[kEd448PointSize];
  size_t raw_public_key_size = point_size;
  if (EVP_PKEY_get_raw_public_key(
          get(), raw_public_key, &raw_public_key_size) != 1) {
    return false;
  }

  return IsSmallOrderEdDsaPoint(*this, raw_public_key, raw_public_key_size);
}

int EVPKeyPointer::bits() const {
  if (get() == nullptr) return 0;
  return EVP_PKEY_bits(get());
}

size_t EVPKeyPointer::size() const {
  if (get() == nullptr) return 0;
  return EVP_PKEY_size(get());
}

EVPKeyCtxPointer EVPKeyPointer::newCtx() const {
  if (!pkey_) return {};
  return EVPKeyCtxPointer::New(*this);
}

size_t EVPKeyPointer::rawPublicKeySize() const {
  if (!pkey_) return 0;
  size_t len = 0;
  if (EVP_PKEY_get_raw_public_key(get(), nullptr, &len) == 1) return len;
  return 0;
}

size_t EVPKeyPointer::rawPrivateKeySize() const {
  if (!pkey_) return 0;
  size_t len = 0;
  if (EVP_PKEY_get_raw_private_key(get(), nullptr, &len) == 1) return len;
  return 0;
}

DataPointer EVPKeyPointer::rawPublicKey() const {
  if (!pkey_) return {};
  if (auto data = DataPointer::Alloc(rawPublicKeySize())) {
    const Buffer<unsigned char> buf = data;
    size_t len = data.size();
    if (EVP_PKEY_get_raw_public_key(get(), buf.data, &len) != 1) return {};
    return data;
  }
  return {};
}

namespace {
DataPointer GetRawSeed(EVP_PKEY* key, size_t seed_len) {
  auto data = DataPointer::Alloc(seed_len);
  if (!data) return {};
  const Buffer<unsigned char> buf = data;
  size_t len = data.size();
#if NCRYPTO_USE_BORINGSSL
  if (EVP_PKEY_get_private_seed(key, buf.data, &len) != 1) return {};
#elif NCRYPTO_USE_OPENSSL_PROVIDER
  if (EVP_PKEY_get_octet_string_param(key, "seed", buf.data, buf.len, &len) !=
      1)
    return {};
#else
#error "Unsupported crypto backend"
#endif
  return data;
}

}  // namespace

Result<DataPointer, EVPKeyPointer::RawExportError> EVPKeyPointer::rawSeed()
    const {
  const auto* algorithm = getAlgorithm();
  if (algorithm == nullptr || algorithm->seedSize() == 0) {
    return RawExportError::UNSUPPORTED_KEY_TYPE;
  }
  auto data = GetRawSeed(get(), algorithm->seedSize());
  if (!data) return RawExportError::MISSING_SEED;
  return data;
}

Result<EVPKeyPointer::RawJwkData, EVPKeyPointer::RawExportError>
EVPKeyPointer::exportRawJwk(bool include_private) const {
  const auto* algorithm = getAlgorithm();
  if (algorithm == nullptr || (!algorithm->isOkp() && !algorithm->isPqc())) {
    return RawExportError::UNSUPPORTED_KEY_TYPE;
  }
  RawJwkData data{algorithm, {}, {}};
  if (include_private) {
    const size_t seed_len = algorithm->seedSize();
    data.private_key =
        seed_len != 0 ? GetRawSeed(get(), seed_len) : rawPrivateKey();
    if (!data.private_key) {
      return seed_len != 0 ? RawExportError::MISSING_SEED
                           : RawExportError::FAILED;
    }
  }
  data.public_key = rawPublicKey();
  if (!data.public_key) return RawExportError::FAILED;
  return data;
}

EVPKeyPointer EVPKeyPointer::NewRawJwk(
    const KeyAlgorithm& algorithm,
    const Buffer<const unsigned char>& public_key,
    const std::optional<Buffer<const unsigned char>>& private_key) {
  if (!algorithm.isOkp() && !algorithm.isPqc()) return {};
  if (!private_key) return NewRawPublic(algorithm, public_key);
  auto key = algorithm.seedSize() != 0 ? NewRawSeed(algorithm, *private_key)
                                       : NewRawPrivate(algorithm, *private_key);
  if (!key) return {};
  const auto derived_public = key.rawPublicKey();
  if (!derived_public || derived_public.size() != public_key.len ||
      CRYPTO_memcmp(derived_public.get(), public_key.data, public_key.len) !=
          0) {
    return {};
  }
  return key;
}

DataPointer EVPKeyPointer::rawPrivateKey() const {
  if (!pkey_) return {};
  if (auto data = DataPointer::Alloc(rawPrivateKeySize())) {
    const Buffer<unsigned char> buf = data;
    size_t len = data.size();
    if (EVP_PKEY_get_raw_private_key(get(), buf.data, &len) != 1) return {};
    return data;
  }
  return {};
}

BIOPointer EVPKeyPointer::derPublicKey() const {
  if (!pkey_) return {};
  auto bio = BIOPointer::NewMem();
  if (!bio) return {};
  if (!i2d_PUBKEY_bio(bio.get(), get())) return {};
  return bio;
}

bool EVPKeyPointer::assign(const ECKeyPointer& eckey) {
  if (!pkey_ || !eckey) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return set(eckey);
#elif NCRYPTO_USE_BORINGSSL
  return EVP_PKEY_assign_EC_KEY(pkey_.get(), eckey.get());
#else
#error "Unsupported crypto backend"
#endif
}

bool EVPKeyPointer::set(const ECKeyPointer& eckey) {
  if (!pkey_ || !eckey) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  const int nid = EC_GROUP_get_curve_name(eckey.group_.get());
  const char* group_name = OBJ_nid2sn(nid);
  if (group_name == nullptr) return false;

  OSSLParamBldPointer bld(OSSL_PARAM_BLD_new());
  if (!bld || OSSL_PARAM_BLD_push_utf8_string(
                  bld.get(), OSSL_PKEY_PARAM_GROUP_NAME, group_name, 0) != 1) {
    return false;
  }

  int selection = EVP_PKEY_KEY_PARAMETERS;
  OpenSSLBufferPointer encoded_public_key;
  ECPointPointer generated_public_key;
  const EC_POINT* public_key = eckey.pub_.get();
  if (public_key == nullptr && eckey.priv_ != nullptr) {
    generated_public_key = ECPointPointer::New(eckey.group_.get());
    if (!generated_public_key ||
        !generated_public_key.mul(eckey.group_.get(), eckey.priv_.get())) {
      return false;
    }
    public_key = generated_public_key.get();
  }

  if (public_key != nullptr) {
    unsigned char* encoded_public_key_raw = nullptr;
    const size_t encoded_public_key_len =
        EC_POINT_point2buf(eckey.group_.get(),
                           public_key,
                           POINT_CONVERSION_UNCOMPRESSED,
                           &encoded_public_key_raw,
                           nullptr);
    if (encoded_public_key_len == 0) return false;
    encoded_public_key.reset(encoded_public_key_raw);
    if (OSSL_PARAM_BLD_push_octet_string(bld.get(),
                                         OSSL_PKEY_PARAM_PUB_KEY,
                                         encoded_public_key.get(),
                                         encoded_public_key_len) != 1) {
      return false;
    }
    selection |= EVP_PKEY_PUBLIC_KEY;
  }

  if (eckey.priv_ != nullptr) {
    if (OSSL_PARAM_BLD_push_BN(
            bld.get(), OSSL_PKEY_PARAM_PRIV_KEY, eckey.priv_.get()) != 1) {
      return false;
    }
    selection |= EVP_PKEY_PRIVATE_KEY;
  }

  OSSLParamPointer params(OSSL_PARAM_BLD_to_param(bld.get()));
  if (!params) return false;
  auto pkey = NewPKeyFromData(KeyAlgorithm::EC, selection, params.get());
  if (!pkey) return false;
  reset(pkey.release());
  return true;
#elif NCRYPTO_USE_BORINGSSL
  return EVP_PKEY_set1_EC_KEY(pkey_.get(), eckey);
#else
#error "Unsupported crypto backend"
#endif
}

#if NCRYPTO_USE_LEGACY_KEY_TYPES
EVPKeyPointer::operator const EC_KEY*() const {
  if (!pkey_) return nullptr;
  return EVP_PKEY_get0_EC_KEY(pkey_.get());
}
#endif  // NCRYPTO_USE_LEGACY_KEY_TYPES

namespace {

EVP_PKEY* DecodeRsaPublicKey(const unsigned char** data, size_t length) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  // Borrow the EVP_PKEY constructor and its data from a context that stays
  // alive until after the restricted decoder context is destroyed.
  EVP_PKEY* raw = nullptr;
  DeleteFnPtr<OSSL_DECODER_CTX, OSSL_DECODER_CTX_free> construct_ctx(
      OSSL_DECODER_CTX_new_for_pkey(&raw,
                                    "DER",
                                    "type-specific",
                                    KeyAlgorithm::RSA.name(),
                                    EVP_PKEY_PUBLIC_KEY,
                                    nullptr,
                                    nullptr));
  if (!construct_ctx) return nullptr;
  auto* construct = OSSL_DECODER_CTX_get_construct(construct_ctx.get());
  void* construct_data =
      OSSL_DECODER_CTX_get_construct_data(construct_ctx.get());
  if (construct == nullptr || construct_data == nullptr) return nullptr;

  // Add only the type-specific RSA decoder: new_for_pkey() can also build
  // chains that accept SPKI. The owning context retains the cleanup callback.
  DeleteFnPtr<OSSL_DECODER, OSSL_DECODER_free> decoder(OSSL_DECODER_fetch(
      nullptr, KeyAlgorithm::RSA.name(), "input=der,structure=type-specific"));
  DeleteFnPtr<OSSL_DECODER_CTX, OSSL_DECODER_CTX_free> ctx(
      OSSL_DECODER_CTX_new());
  if (!decoder || !ctx ||
      OSSL_DECODER_CTX_add_decoder(ctx.get(), decoder.get()) != 1 ||
      OSSL_DECODER_CTX_set_input_type(ctx.get(), "DER") != 1 ||
      OSSL_DECODER_CTX_set_selection(ctx.get(), EVP_PKEY_PUBLIC_KEY) != 1 ||
      OSSL_DECODER_CTX_set_construct(ctx.get(), construct) != 1 ||
      OSSL_DECODER_CTX_set_construct_data(ctx.get(), construct_data) != 1) {
    return nullptr;
  }
  const int result = OSSL_DECODER_from_data(ctx.get(), data, &length);
  EVPKeyPointer key(raw);
  return result == 1 ? key.release() : nullptr;
#elif NCRYPTO_USE_BORINGSSL
  return d2i_PublicKey(NID_rsaEncryption, nullptr, data, length);
#else
#error "Unsupported crypto backend"
#endif
}

EVPKeyPointer::ParseKeyResult TryParsePublicKeyInner(const BIOPointer& bp,
                                                     const char* name,
                                                     auto&& parse) {
  if (!bp.resetBio()) {
    return EVPKeyPointer::ParseKeyResult(EVPKeyPointer::PKParseError::FAILED);
  }
  unsigned char* der_data;
  long der_len;  // NOLINT(runtime/int)

  // This skips surrounding data and decodes PEM to DER.
  {
    MarkPopErrorOnReturn mark_pop_error_on_return;
    if (PEM_bytes_read_bio(
            &der_data, &der_len, nullptr, name, bp.get(), nullptr, nullptr) !=
        1)
      return EVPKeyPointer::ParseKeyResult(
          EVPKeyPointer::PKParseError::NOT_RECOGNIZED);
  }
  DataPointer data(der_data, der_len);

  // OpenSSL might modify the pointer, so we need to make a copy before parsing.
  const unsigned char* p = der_data;
  EVPKeyPointer pkey(parse(&p, der_len));
  if (!pkey)
    return EVPKeyPointer::ParseKeyResult(EVPKeyPointer::PKParseError::FAILED);
  return EVPKeyPointer::ParseKeyResult(std::move(pkey));
}

constexpr bool IsASN1Sequence(const unsigned char* data,
                              size_t size,
                              size_t* data_offset,
                              size_t* data_size) {
  if (size < 2 || data[0] != 0x30) return false;

  if (data[1] & 0x80) {
    // Long form.
    size_t n_bytes = data[1] & ~0x80;
    if (n_bytes + 2 > size || n_bytes > sizeof(size_t)) return false;
    size_t length = 0;
    for (size_t i = 0; i < n_bytes; i++) length = (length << 8) | data[i + 2];
    *data_offset = 2 + n_bytes;
    *data_size = std::min(size - 2 - n_bytes, length);
  } else {
    // Short form.
    *data_offset = 2;
    *data_size = std::min<size_t>(size - 2, data[1]);
  }

  return true;
}

constexpr bool IsEncryptedPrivateKeyInfo(
    const Buffer<const unsigned char>& buffer) {
  // Both PrivateKeyInfo and EncryptedPrivateKeyInfo start with a SEQUENCE.
  if (buffer.len == 0 || buffer.data == nullptr) return false;
  size_t offset, len;
  if (!IsASN1Sequence(buffer.data, buffer.len, &offset, &len)) return false;

  // A PrivateKeyInfo sequence always starts with an integer whereas an
  // EncryptedPrivateKeyInfo starts with an AlgorithmIdentifier.
  return len >= 1 && buffer.data[offset] != 2;
}

}  // namespace

bool EVPKeyPointer::IsRSAPrivateKey(const Buffer<const unsigned char>& buffer) {
  // Both RSAPrivateKey and RSAPublicKey structures start with a SEQUENCE.
  size_t offset, len;
  if (!IsASN1Sequence(buffer.data, buffer.len, &offset, &len)) return false;

  // An RSAPrivateKey sequence always starts with a single-byte integer whose
  // value is either 0 or 1, whereas an RSAPublicKey starts with the modulus
  // (which is the product of two primes and therefore at least 4), so we can
  // decide the type of the structure based on the first three bytes of the
  // sequence.
  return len >= 3 && buffer.data[offset] == 2 && buffer.data[offset + 1] == 1 &&
         !(buffer.data[offset + 2] & 0xfe);
}

EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKeyPEM(
    const Buffer<const unsigned char>& buffer) {
  auto bp = BIOPointer::New(buffer.data, buffer.len);
  if (!bp) return ParseKeyResult(PKParseError::FAILED);

  // Try parsing as SubjectPublicKeyInfo (SPKI) first.
  if (auto ret = TryParsePublicKeyInner(
          bp,
          "PUBLIC KEY",
          [](const unsigned char** p, long l) {  // NOLINT(runtime/int)
            return d2i_PUBKEY(nullptr, p, l);
          })) {
    return ret;
  }

  // Maybe it is PKCS#1.
  if (auto ret = TryParsePublicKeyInner(
          bp,
          "RSA PUBLIC KEY",
          [](const unsigned char** p, long l) {  // NOLINT(runtime/int)
            return DecodeRsaPublicKey(p, l);
          })) {
    return ret;
  }

  // X.509 fallback.
  if (auto ret = TryParsePublicKeyInner(
          bp,
          "CERTIFICATE",
          [](const unsigned char** p, long l) {  // NOLINT(runtime/int)
            X509Pointer x509(d2i_X509(nullptr, p, l));
            return x509 ? X509_get_pubkey(x509.get()) : nullptr;
          })) {
    return ret;
  };

  return ParseKeyResult(PKParseError::NOT_RECOGNIZED);
}

EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePublicKey(
    const PublicKeyEncodingConfig& config,
    const Buffer<const unsigned char>& buffer) {
  if (config.format == PKFormatType::PEM) {
    return TryParsePublicKeyPEM(buffer);
  }

  if (config.format != PKFormatType::DER) {
    return ParseKeyResult(PKParseError::FAILED);
  }

  const unsigned char* start = buffer.data;

  EVP_PKEY* key = nullptr;

  if (config.type == PKEncodingType::PKCS1 &&
      (key = DecodeRsaPublicKey(&start, buffer.len))) {
    return EVPKeyPointer::ParseKeyResult(EVPKeyPointer(key));
  }

  if (config.type == PKEncodingType::SPKI &&
      (key = d2i_PUBKEY(nullptr, &start, buffer.len))) {
    return EVPKeyPointer::ParseKeyResult(EVPKeyPointer(key));
  }

  return ParseKeyResult(PKParseError::FAILED);
}

namespace {
Buffer<char> GetPassphrase(
    const EVPKeyPointer::PrivateKeyEncodingConfig& config) {
  Buffer<char> pass{
      // OpenSSL will not actually dereference this pointer, so it can be any
      // non-null pointer. We cannot assert that directly, which is why we
      // intentionally use a pointer that will likely cause a segmentation fault
      // when dereferenced.
      .data = reinterpret_cast<char*>(-1),
      .len = 0,
  };
  if (config.passphrase.has_value()) {
    auto& passphrase = config.passphrase.value();
    // The pass.data can't be a nullptr, even if the len is zero or else
    // openssl will prompt for a password and we really don't want that.
    if (passphrase.get() != nullptr) {
      pass.data = static_cast<char*>(passphrase.get());
    }
    pass.len = passphrase.size();
  }
  return pass;
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
using OSSLEncoderCtxPointer =
    DeleteFnPtr<OSSL_ENCODER_CTX, OSSL_ENCODER_CTX_free>;

bool WriteEncodedPKey(BIO* bio,
                      const EVP_PKEY* pkey,
                      int selection,
                      EVPKeyPointer::PKFormatType format,
                      const char* structure,
                      const EVP_CIPHER* cipher = nullptr,
                      Buffer<char> passphrase = {}) {
  const char* output_type =
      format == EVPKeyPointer::PKFormatType::PEM ? "PEM" : "DER";
  OSSLEncoderCtxPointer ctx(OSSL_ENCODER_CTX_new_for_pkey(
      pkey, selection, output_type, structure, nullptr));
  if (!ctx || OSSL_ENCODER_CTX_get_num_encoders(ctx.get()) == 0) {
    return false;
  }

  if (cipher != nullptr) {
    if (OSSL_ENCODER_CTX_set_cipher(
            ctx.get(), EVP_CIPHER_get0_name(cipher), nullptr) != 1 ||
        OSSL_ENCODER_CTX_set_passphrase(
            ctx.get(),
            reinterpret_cast<const unsigned char*>(passphrase.data),
            passphrase.len) != 1) {
      return false;
    }
  }

  return OSSL_ENCODER_to_bio(ctx.get(), bio) == 1;
}

struct DERView {
  const unsigned char* data = nullptr;
  size_t len = 0;
};

int WriteDERView(const void* x, unsigned char** out) {
  const auto* der = static_cast<const DERView*>(x);
  if (der == nullptr || der->data == nullptr ||
      der->len > static_cast<size_t>(INT_MAX)) {
    return -1;
  }
  if (out != nullptr) {
    memcpy(*out, der->data, der->len);
    *out += der->len;
  }
  return static_cast<int>(der->len);
}

bool WriteEncryptedTraditionalPEM(BIO* bio,
                                  const EVP_PKEY* pkey,
                                  const EVP_CIPHER* cipher,
                                  Buffer<char> passphrase) {
  if (passphrase.len > static_cast<size_t>(INT_MAX)) return false;

  unsigned char* der = nullptr;
  size_t der_len = 0;
  OSSLEncoderCtxPointer ctx(OSSL_ENCODER_CTX_new_for_pkey(
      pkey, OSSL_KEYMGMT_SELECT_KEYPAIR, "DER", "pkcs1", nullptr));
  if (!ctx) return false;

  const int result = OSSL_ENCODER_to_data(ctx.get(), &der, &der_len);
  DataPointer der_storage(der, der_len);
  if (result != 1) return false;

  DERView der_view{der_storage.get<const unsigned char>(), der_len};
  return PEM_ASN1_write_bio(
             WriteDERView,
             PEM_STRING_RSA,
             bio,
             &der_view,
             cipher,
             reinterpret_cast<const unsigned char*>(passphrase.data),
             static_cast<int>(passphrase.len),
             nullptr,
             nullptr) == 1;
}

bool ECKeyHasMissingOid(const EVPKeyPointer& key) {
  if (!key.isA(KeyAlgorithm::EC)) return false;

  const Ec ec(key.get());
  const EC_GROUP* group = ec.getGroup();
  if (group == nullptr ||
      EC_GROUP_get_asn1_flag(group) != OPENSSL_EC_NAMED_CURVE) {
    return false;
  }

  const int nid = EC_GROUP_get_curve_name(group);
  if (nid == NID_undef) return true;

  const ASN1_OBJECT* asn1 = OBJ_nid2obj(nid);
  return asn1 == nullptr || OBJ_length(asn1) == 0;
}
#endif
}  // namespace

EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey(
    const PrivateKeyEncodingConfig& config,
    const Buffer<const unsigned char>& buffer) {
  ERR_clear_error();
  static constexpr auto keyOrError = [](EVPKeyPointer pkey,
                                        bool had_passphrase = false) {
    if (unsigned long err = ERR_peek_error()) {  // NOLINT(runtime/int)
      if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
          ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ && !had_passphrase) {
        return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
      }
      return ParseKeyResult(PKParseError::FAILED, err);
    }
    if (!pkey) return ParseKeyResult(PKParseError::FAILED);
    return ParseKeyResult(std::move(pkey));
  };

  auto bio = BIOPointer::New(buffer);
  if (!bio) return ParseKeyResult(PKParseError::FAILED);

  auto passphrase = GetPassphrase(config);

  if (config.format == PKFormatType::PEM) {
    auto key = PEM_read_bio_PrivateKey(
        bio.get(),
        nullptr,
        PasswordCallback,
        config.passphrase.has_value() ? &passphrase : nullptr);
    return keyOrError(EVPKeyPointer(key), config.passphrase.has_value());
  }

  if (config.format != PKFormatType::DER) {
    return ParseKeyResult(PKParseError::FAILED);
  }

  switch (config.type) {
    case PKEncodingType::PKCS1: {
      auto key = d2i_PrivateKey_bio(bio.get(), nullptr);
      return keyOrError(EVPKeyPointer(key));
    }
    case PKEncodingType::PKCS8: {
      if (IsEncryptedPrivateKeyInfo(buffer)) {
        auto key = d2i_PKCS8PrivateKey_bio(
            bio.get(),
            nullptr,
            PasswordCallback,
            config.passphrase.has_value() ? &passphrase : nullptr);
        return keyOrError(EVPKeyPointer(key), config.passphrase.has_value());
      }

      PKCS8Pointer p8inf(d2i_PKCS8_PRIV_KEY_INFO_bio(bio.get(), nullptr));
      if (!p8inf) {
        return ParseKeyResult(PKParseError::FAILED, ERR_peek_error());
      }
      return keyOrError(EVPKeyPointer(EVP_PKCS82PKEY(p8inf.get())));
    }
    case PKEncodingType::SEC1: {
      auto key = d2i_PrivateKey_bio(bio.get(), nullptr);
      return keyOrError(EVPKeyPointer(key));
    }
    default: {
      return ParseKeyResult(PKParseError::FAILED, ERR_peek_error());
    }
  };
}

EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryLoadPrivateKeyFromStore(
    const StorePrivateKeyConfig& config) {
#if NCRYPTO_USE_BORINGSSL
  return ParseKeyResult(PKParseError::FAILED);
#elif NCRYPTO_USE_OPENSSL_PROVIDER
  // The error queue is left populated on failure so the caller can surface a
  // `code` and an `opensslErrorStack`, matching TryParsePrivateKey(), and is
  // cleared on success because decoders leave entries behind either way.
  std::string uri_str(config.uri);
  std::string properties_str;
  const char* properties = nullptr;
  if (config.properties.has_value() && !config.properties->empty()) {
    properties_str.assign(config.properties->data(), config.properties->size());
    properties = properties_str.c_str();
  }

  // config.passphrase outlives this call, so no copy is needed.
  Buffer<char> passbuf{.data = nullptr, .len = 0};
  if (config.passphrase.has_value()) {
    passbuf.data = const_cast<char*>(config.passphrase->data);
    passbuf.len = config.passphrase->len;
  }
  StorePassphraseData passphrase_data{
      .passphrase = passbuf,
      .has_passphrase = config.passphrase.has_value(),
  };
  // Declared before ctx so that reverse destruction closes the store first;
  // it holds both for its lifetime.
  UIMethodPointer ui_method(
      UI_UTIL_wrap_read_pem_callback(StorePasswordCallback, 0));
  if (!ui_method) return ParseKeyResult(PKParseError::FAILED);

  // Errors from loaders that declined the URI are retained oldest-first, so the
  // newest entry is the loader that actually handled it. Must run before ctx is
  // destroyed, since OSSL_STORE_close() can push errors of its own.
  const auto failed = [&](bool missing_passphrase) {
    if (missing_passphrase)
      return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
    return ParseKeyResult(PKParseError::FAILED, ERR_peek_last_error());
  };

  const OSSL_PARAM store_params[] = {OSSL_PARAM_END};
  StoreCtxPointer ctx(OSSL_STORE_open_ex(uri_str.c_str(),
                                         nullptr,
                                         properties,
                                         ui_method.get(),
                                         &passphrase_data,
                                         store_params,
                                         nullptr,
                                         nullptr));
  if (!ctx) return failed(passphrase_data.missing_passphrase);

  if (!OSSL_STORE_expect(ctx.get(), OSSL_STORE_INFO_PKEY)) {
    return failed(passphrase_data.missing_passphrase);
  }

  EVPKeyPointer pkey;
  bool store_error = false;
  while (!OSSL_STORE_eof(ctx.get())) {
    OSSL_STORE_INFO* info = OSSL_STORE_load(ctx.get());
    if (info == nullptr) {
      if (OSSL_STORE_error(ctx.get())) {
        store_error = true;
        break;
      }
      continue;
    }
    if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
      EVP_PKEY* raw_pkey = OSSL_STORE_INFO_get1_PKEY(info);
      if (raw_pkey != nullptr) {
        pkey = EVPKeyPointer(raw_pkey);
      } else {
        store_error = true;
      }
    }
    OSSL_STORE_INFO_free(info);
    if (pkey || store_error) break;
  }

  // missing_passphrase is sticky, so a key that loaded anyway wins over it.
  if (pkey) {
    ctx.reset();
    ERR_clear_error();
    return ParseKeyResult(std::move(pkey));
  }

  if (passphrase_data.missing_passphrase || store_error) {
    return failed(passphrase_data.missing_passphrase);
  }
  return ParseKeyResult(PKParseError::NOT_RECOGNIZED);
#else
#error "Unsupported crypto backend"
#endif
}

Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
    const PrivateKeyEncodingConfig& config) const {
  if (config.format == PKFormatType::JWK) {
    return Result<BIOPointer, bool>(false);
  }

  auto bio = BIOPointer::NewMem();
  if (!bio) {
    return Result<BIOPointer, bool>(false);
  }

  auto passphrase = GetPassphrase(config);
  MarkPopErrorOnReturn mark_pop_error_on_return;
  bool err;

  switch (config.type) {
    case PKEncodingType::PKCS1: {
      // PKCS1 is only permitted for RSA keys.
      if (!isA(KeyAlgorithm::RSA)) return Result<BIOPointer, bool>(false);

#if NCRYPTO_USE_OPENSSL_PROVIDER
      const EVP_CIPHER* cipher =
          config.format == PKFormatType::PEM ? config.cipher.get() : nullptr;
      if (cipher != nullptr && passphrase.len == 0) {
        err =
            !WriteEncryptedTraditionalPEM(bio.get(), get(), cipher, passphrase);
      } else {
        err = !WriteEncodedPKey(bio.get(),
                                get(),
                                OSSL_KEYMGMT_SELECT_ALL,
                                config.format,
                                "pkcs1",
                                cipher,
                                passphrase);
      }
#elif NCRYPTO_USE_BORINGSSL
      RSA* rsa = EVP_PKEY_get0_RSA(get());
      if (rsa == nullptr) return Result<BIOPointer, bool>(false);

      switch (config.format) {
        case PKFormatType::PEM: {
          err = PEM_write_bio_RSAPrivateKey(
                    bio.get(),
                    rsa,
                    config.cipher,
                    reinterpret_cast<unsigned char*>(passphrase.data),
                    passphrase.len,
                    nullptr,
                    nullptr) != 1;
          break;
        }
        case PKFormatType::DER: {
          // Encoding PKCS1 as DER. This variation does not permit encryption.
          err = i2d_RSAPrivateKey_bio(bio.get(), rsa) != 1;
          break;
        }
        default: {
          // Should never get here.
          return Result<BIOPointer, bool>(false);
        }
      }
#else
#error "Unsupported crypto backend"
#endif
      break;
    }
    case PKEncodingType::PKCS8: {
      EVP_PKEY* export_key = get();
#if NCRYPTO_USE_OPENSSL_PROVIDER
      // OpenSSL's provider EC PKCS8 encoders temporarily change encoding flags.
      // Use an independent key so concurrent exports do not change the source.
      EVPKeyPointer key_copy;
      if ((isA(KeyAlgorithm::EC) || isA(KeyAlgorithm::SM2)) &&
          EVP_PKEY_get0_provider(get()) != nullptr) {
        key_copy.reset(EVP_PKEY_dup(get()));
        if (!key_copy) {
          return Result<BIOPointer, bool>(false,
                                          mark_pop_error_on_return.peekError());
        }
        export_key = key_copy.get();
      }
#endif
      switch (config.format) {
        case PKFormatType::PEM: {
          // Encode PKCS#8 as PEM.
          err = PEM_write_bio_PKCS8PrivateKey(bio.get(),
                                              export_key,
                                              config.cipher,
                                              passphrase.data,
                                              passphrase.len,
                                              nullptr,
                                              nullptr) != 1;
          break;
        }
        case PKFormatType::DER: {
          err = i2d_PKCS8PrivateKey_bio(bio.get(),
                                        export_key,
                                        config.cipher,
                                        passphrase.data,
                                        passphrase.len,
                                        nullptr,
                                        nullptr) != 1;
          break;
        }
        default: {
          // Should never get here.
          return Result<BIOPointer, bool>(false);
        }
      }
      break;
    }
    case PKEncodingType::SEC1: {
      // SEC1 is only permitted for EC keys
      if (!isA(KeyAlgorithm::EC)) return Result<BIOPointer, bool>(false);

#if NCRYPTO_USE_OPENSSL_PROVIDER
      const EVP_CIPHER* cipher =
          config.format == PKFormatType::PEM ? config.cipher.get() : nullptr;
      err = !WriteEncodedPKey(bio.get(),
                              get(),
                              OSSL_KEYMGMT_SELECT_ALL,
                              config.format,
                              "type-specific",
                              cipher,
                              passphrase);
#elif NCRYPTO_USE_BORINGSSL
      EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get());
      if (ec == nullptr) return Result<BIOPointer, bool>(false);

      switch (config.format) {
        case PKFormatType::PEM: {
          err = PEM_write_bio_ECPrivateKey(
                    bio.get(),
                    ec,
                    config.cipher,
                    reinterpret_cast<unsigned char*>(passphrase.data),
                    passphrase.len,
                    nullptr,
                    nullptr) != 1;
          break;
        }
        case PKFormatType::DER: {
          // Encoding SEC1 as DER. This variation does not permit encryption.
          err = i2d_ECPrivateKey_bio(bio.get(), ec) != 1;
          break;
        }
        default: {
          // Should never get here.
          return Result<BIOPointer, bool>(false);
        }
      }
#else
#error "Unsupported crypto backend"
#endif
      break;
    }
    default: {
      // Not a valid private key encoding
      return Result<BIOPointer, bool>(false);
    }
  }

  if (err) {
    // Failed to encode the private key.
    return Result<BIOPointer, bool>(false,
                                    mark_pop_error_on_return.peekError());
  }

  return bio;
}

Result<BIOPointer, bool> EVPKeyPointer::writePublicKey(
    const ncrypto::EVPKeyPointer::PublicKeyEncodingConfig& config) const {
  auto bio = BIOPointer::NewMem();
  if (!bio) return Result<BIOPointer, bool>(false);

  MarkPopErrorOnReturn mark_pop_error_on_return;

  if (config.type == ncrypto::EVPKeyPointer::PKEncodingType::PKCS1) {
    // PKCS#1 is only valid for RSA keys.
#if NCRYPTO_USE_OPENSSL_PROVIDER
    if (!isA(KeyAlgorithm::RSA)) return Result<BIOPointer, bool>(false);
    if (!WriteEncodedPKey(bio.get(),
                          get(),
                          OSSL_KEYMGMT_SELECT_PUBLIC_KEY,
                          config.format,
                          "pkcs1")) {
      return Result<BIOPointer, bool>(false,
                                      mark_pop_error_on_return.peekError());
    }
    return bio;
#elif NCRYPTO_USE_BORINGSSL
    RSA* rsa = EVP_PKEY_get0_RSA(get());
    if (rsa == nullptr) return Result<BIOPointer, bool>(false);

    if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) {
      // Encode PKCS#1 as PEM.
      if (PEM_write_bio_RSAPublicKey(bio.get(), rsa) != 1) {
        return Result<BIOPointer, bool>(false,
                                        mark_pop_error_on_return.peekError());
      }
      return bio;
    }

    // Encode PKCS#1 as DER.
    if (i2d_RSAPublicKey_bio(bio.get(), rsa) != 1) {
      return Result<BIOPointer, bool>(false,
                                      mark_pop_error_on_return.peekError());
    }
    return bio;
#else
#error "Unsupported crypto backend"
#endif
  }

#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (ECKeyHasMissingOid(*this)) {
    ERR_raise(ERR_LIB_EC, EC_R_MISSING_OID);
    return Result<BIOPointer, bool>(false,
                                    mark_pop_error_on_return.peekError());
  }
#endif

  if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) {
    // Encode SPKI as PEM.
#if NCRYPTO_USE_OPENSSL_PROVIDER
    // Build the SubjectPublicKeyInfo wrapper explicitly before PEM encoding.
    // Provider-backed keys can fail the direct PEM_write_bio_PUBKEY() path even
    // when OpenSSL can materialize the public wrapper with X509_PUBKEY_set().
    X509_PUBKEY* pubkey = nullptr;
    if (X509_PUBKEY_set(&pubkey, get()) != 1) {
      X509_PUBKEY_free(pubkey);
      return Result<BIOPointer, bool>(false,
                                      mark_pop_error_on_return.peekError());
    }
    X509PubKeyPointer pubkey_ptr(pubkey);
    if (PEM_write_bio_X509_PUBKEY(bio.get(), pubkey_ptr.get()) != 1) {
      return Result<BIOPointer, bool>(false,
                                      mark_pop_error_on_return.peekError());
    }
#elif NCRYPTO_USE_BORINGSSL
    // BoringSSL does not declare PEM_write_bio_X509_PUBKEY().
    if (PEM_write_bio_PUBKEY(bio.get(), get()) != 1) {
      return Result<BIOPointer, bool>(false,
                                      mark_pop_error_on_return.peekError());
    }
#else
#error "Unsupported crypto backend"
#endif
    return bio;
  }

  // Encode SPKI as DER.
  if (i2d_PUBKEY_bio(bio.get(), get()) != 1) {
    return Result<BIOPointer, bool>(false,
                                    mark_pop_error_on_return.peekError());
  }
  return bio;
}

bool EVPKeyPointer::isRsaVariant(const EVP_PKEY* key) {
  return isA(key, KeyAlgorithm::RSA) || isA(key, KeyAlgorithm::RSA_PSS);
}

bool EVPKeyPointer::isRsaVariant() const {
  return isRsaVariant(get());
}

bool EVPKeyPointer::isSigVariant() const {
  return isA(KeyAlgorithm::EC) || isA(KeyAlgorithm::DSA);
}

int EVPKeyPointer::getDefaultSignPadding() const {
  return isA(KeyAlgorithm::RSA_PSS) ? RSA_PKCS1_PSS_PADDING : RSA_PKCS1_PADDING;
}

std::optional<uint32_t> EVPKeyPointer::getBytesOfRS() const {
  if (!pkey_) return std::nullopt;
  int bits;

  if (isA(KeyAlgorithm::DSA)) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
    DeleteFnPtr<BIGNUM, BN_free> q;
    if (!GetPKeyBnParam(get(), OSSL_PKEY_PARAM_FFC_Q, &q)) return std::nullopt;
    bits = BignumPointer::GetBitCount(q.get());
#elif NCRYPTO_USE_BORINGSSL
    const DSA* dsa_key = EVP_PKEY_get0_DSA(get());
    bool has_bits = false;
    // Both r and s are computed mod q, so their width is limited by that of q.
    if (dsa_key != nullptr) {
      const BIGNUM* q = DSA_get0_q(dsa_key);
      if (q != nullptr) {
        bits = BignumPointer::GetBitCount(q);
        has_bits = true;
      }
    }
    if (!has_bits) return std::nullopt;
#else
#error "Unsupported crypto backend"
#endif
  } else if (isA(KeyAlgorithm::EC)) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
    bits = EVP_PKEY_bits(get());
#elif NCRYPTO_USE_BORINGSSL
    const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(get());
    if (ec_key == nullptr) return std::nullopt;
    const EC_GROUP* group = ECKeyPointer::GetGroup(ec_key);
    if (group == nullptr) return std::nullopt;
    bits = EC_GROUP_order_bits(group);
#else
#error "Unsupported crypto backend"
#endif
  } else {
    return std::nullopt;
  }

  if (bits <= 0) return std::nullopt;

  return (bits + 7) / 8;
}

EVPKeyPointer::operator Rsa() const {
  if (!isA(KeyAlgorithm::RSA) && !isA(KeyAlgorithm::RSA_PSS)) return {};

#if NCRYPTO_USE_OPENSSL_PROVIDER
  return Rsa(get());
#elif NCRYPTO_USE_BORINGSSL
  OSSL3_CONST RSA* rsa = EVP_PKEY_get0_RSA(get());
  if (rsa == nullptr) return {};
  return Rsa(rsa);
#else
#error "Unsupported crypto backend"
#endif
}

EVPKeyPointer::operator Dsa() const {
  if (!isA(KeyAlgorithm::DSA)) return {};

#if NCRYPTO_USE_OPENSSL_PROVIDER
  return Dsa(get());
#elif NCRYPTO_USE_BORINGSSL
  OSSL3_CONST DSA* dsa = EVP_PKEY_get0_DSA(get());
  if (dsa == nullptr) return {};
  return Dsa(dsa);
#else
#error "Unsupported crypto backend"
#endif
}

bool EVPKeyPointer::validateDsaParameters() const {
  if (!pkey_) return false;
#ifndef OPENSSL_IS_BORINGSSL
  if (EVP_default_properties_is_fips_enabled(nullptr) &&
      isA(KeyAlgorithm::DSA)) {
#elif defined(OPENSSL_IS_BORINGSSL)
  if (FIPS_mode() && isA(KeyAlgorithm::DSA)) {
#endif
    // Validate DSA2 parameters from FIPS 186-4.
#if NCRYPTO_USE_OPENSSL_PROVIDER
    DeleteFnPtr<BIGNUM, BN_free> p;
    DeleteFnPtr<BIGNUM, BN_free> q;
    if (!GetPKeyBnParam(pkey_.get(), OSSL_PKEY_PARAM_FFC_P, &p) ||
        !GetPKeyBnParam(pkey_.get(), OSSL_PKEY_PARAM_FFC_Q, &q)) {
      return false;
    }
    const BIGNUM* p_value = p.get();
    const BIGNUM* q_value = q.get();
#elif NCRYPTO_USE_BORINGSSL
    const DSA* dsa = EVP_PKEY_get0_DSA(pkey_.get());
    if (dsa == nullptr) return false;
    const BIGNUM* p;
    const BIGNUM* q;
    DSA_get0_pqg(dsa, &p, &q, nullptr);
    if (p == nullptr || q == nullptr) return false;
    const BIGNUM* p_value = p;
    const BIGNUM* q_value = q;
#else
#error "Unsupported crypto backend"
#endif
    int L = BignumPointer::GetBitCount(p_value);
    int N = BignumPointer::GetBitCount(q_value);

    return (L == 1024 && N == 160) || (L == 2048 && N == 224) ||
           (L == 2048 && N == 256) || (L == 3072 && N == 256);
  }

  return true;
}

// ============================================================================

SSLPointer::SSLPointer(SSL* ssl) : ssl_(ssl) {}

SSLPointer::SSLPointer(SSLPointer&& other) noexcept : ssl_(other.release()) {}

SSLPointer& SSLPointer::operator=(SSLPointer&& other) noexcept {
  if (this == &other) return *this;
  this->~SSLPointer();
  return *new (this) SSLPointer(std::move(other));
}

SSLPointer::~SSLPointer() {
  reset();
}

void SSLPointer::reset(SSL* ssl) {
  ssl_.reset(ssl);
}

SSL* SSLPointer::release() {
  return ssl_.release();
}

SSLPointer SSLPointer::New(const SSLCtxPointer& ctx) {
  if (!ctx) return {};
  return SSLPointer(SSL_new(ctx.get()));
}

void SSLPointer::getCiphers(std::function<void(const char*)> cb) const {
  if (!ssl_) return;
  STACK_OF(SSL_CIPHER)* ciphers = SSL_get_ciphers(get());

  // TLSv1.3 ciphers aren't listed by EVP. There are only 5, we could just
  // document them, but since there are only 5, easier to just add them manually
  // and not have to explain their absence in the API docs. They are lower-cased
  // because the docs say they will be.
  static constexpr const char* TLS13_CIPHERS[] = {
      "tls_aes_256_gcm_sha384",
      "tls_chacha20_poly1305_sha256",
      "tls_aes_128_gcm_sha256",
      "tls_aes_128_ccm_8_sha256",
      "tls_aes_128_ccm_sha256"};

  const int n = sk_SSL_CIPHER_num(ciphers);

  for (int i = 0; i < n; ++i) {
    const SSL_CIPHER* cipher = sk_SSL_CIPHER_value(ciphers, i);
    cb(SSL_CIPHER_get_name(cipher));
  }

  for (unsigned i = 0; i < 5; ++i) {
    cb(TLS13_CIPHERS[i]);
  }
}

bool SSLPointer::setSession(const SSLSessionPointer& session) {
  if (!session || !ssl_) return false;
  return SSL_set_session(get(), session.get()) == 1;
}

bool SSLPointer::setSniContext(const SSLCtxPointer& ctx) const {
  if (!ctx) return false;
  auto x509 = ncrypto::X509View::From(ctx);
  if (!x509) return false;
  EVP_PKEY* pkey = SSL_CTX_get0_privatekey(ctx.get());
  STACK_OF(X509) * chain;
  int err = SSL_CTX_get0_chain_certs(ctx.get(), &chain);
  if (err == 1) {
    // SSL_use_certificate replaces only the certificate matching the key
    // type. Clear all existing certificates so credentials from the default
    // context cannot be selected for a different key type.
    SSL_certs_clear(get());
    err = SSL_use_certificate(get(), x509);
  }
  if (err == 1) err = SSL_use_PrivateKey(get(), pkey);
  if (err == 1 && chain != nullptr) err = SSL_set1_chain(get(), chain);
  return err == 1;
}

std::optional<uint32_t> SSLPointer::verifyPeerCertificate() const {
  if (!ssl_) return std::nullopt;
  if (X509Pointer::PeerFrom(*this)) {
    return SSL_get_verify_result(get());
  }

  const SSL_CIPHER* curr_cipher = SSL_get_current_cipher(get());
  const SSL_SESSION* sess = SSL_get_session(get());
  // Allow no-cert for PSK authentication in TLS1.2 and lower.
  // In TLS1.3 check that session was reused because TLS1.3 PSK
  // looks like session resumption.
  if (SSL_CIPHER_get_auth_nid(curr_cipher) == NID_auth_psk ||
      (SSL_SESSION_get_protocol_version(sess) == TLS1_3_VERSION &&
       SSL_session_reused(get()))) {
    return X509_V_OK;
  }

  return std::nullopt;
}

std::optional<const std::string_view> SSLPointer::GetServerName(
    const SSL* ssl) {
  if (ssl == nullptr) return std::nullopt;
  auto res = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
  if (res == nullptr) return std::nullopt;
  return res;
}

std::optional<const std::string_view> SSLPointer::getServerName() const {
  if (!ssl_) return std::nullopt;
  return GetServerName(get());
}

X509View SSLPointer::getCertificate() const {
  if (!ssl_) return {};
  ClearErrorOnReturn clear_error_on_return;
  return ncrypto::X509View(SSL_get_certificate(get()));
}

const SSL_CIPHER* SSLPointer::getCipher() const {
  if (!ssl_) return nullptr;
  return SSL_get_current_cipher(get());
}

bool SSLPointer::isServer() const {
  return SSL_is_server(get()) != 0;
}

EVPKeyPointer SSLPointer::getPeerTempKey() const {
  if (!ssl_) return {};
  EVP_PKEY* raw_key = nullptr;
  if (!SSL_get_peer_tmp_key(get(), &raw_key)) return {};
  return EVPKeyPointer(raw_key);
}

std::optional<std::string_view> SSLPointer::getNegotiatedGroup() const {
#if OPENSSL_VERSION_PREREQ(3, 5)
  if (!ssl_) return std::nullopt;
  const char* group = SSL_get0_group_name(get());
  if (group == nullptr) return std::nullopt;
  return group;
#elif defined(OPENSSL_IS_BORINGSSL)
  if (!ssl_) return std::nullopt;
  const int nid = SSL_get_negotiated_group(get());
  if (nid == NID_undef) return std::nullopt;
  const char* group = OBJ_nid2sn(nid);
  if (group == nullptr) return std::nullopt;
  return group;
#else
  return std::nullopt;
#endif
}

std::optional<std::string_view> SSLPointer::getCipherName() const {
  auto cipher = getCipher();
  if (cipher == nullptr) return std::nullopt;
  return SSL_CIPHER_get_name(cipher);
}

std::optional<std::string_view> SSLPointer::getCipherStandardName() const {
  auto cipher = getCipher();
  if (cipher == nullptr) return std::nullopt;
  return SSL_CIPHER_standard_name(cipher);
}

std::optional<std::string_view> SSLPointer::getCipherVersion() const {
  auto cipher = getCipher();
  if (cipher == nullptr) return std::nullopt;
  return SSL_CIPHER_get_version(cipher);
}

std::optional<int> SSLPointer::getSecurityLevel() {
  auto ctx = SSLCtxPointer::New();
  if (!ctx) return std::nullopt;

#ifdef OPENSSL_IS_BORINGSSL
  return SSL_CTX_get_security_level(ctx.get());
#else
  auto ssl = SSLPointer::New(ctx);
  if (!ssl) return std::nullopt;

  return SSL_get_security_level(ssl);
#endif
}

SSLCtxPointer::SSLCtxPointer(SSL_CTX* ctx) : ctx_(ctx) {}

SSLCtxPointer::SSLCtxPointer(SSLCtxPointer&& other) noexcept
    : ctx_(other.release()) {}

SSLCtxPointer& SSLCtxPointer::operator=(SSLCtxPointer&& other) noexcept {
  if (this == &other) return *this;
  this->~SSLCtxPointer();
  return *new (this) SSLCtxPointer(std::move(other));
}

SSLCtxPointer::~SSLCtxPointer() {
  reset();
}

void SSLCtxPointer::reset(SSL_CTX* ctx) {
  ctx_.reset(ctx);
}

void SSLCtxPointer::reset(const SSL_METHOD* method) {
  ctx_.reset(SSL_CTX_new(method));
}

SSL_CTX* SSLCtxPointer::release() {
  return ctx_.release();
}

SSLCtxPointer SSLCtxPointer::NewServer() {
  return SSLCtxPointer(SSL_CTX_new(TLS_server_method()));
}

SSLCtxPointer SSLCtxPointer::NewClient() {
  return SSLCtxPointer(SSL_CTX_new(TLS_client_method()));
}

SSLCtxPointer SSLCtxPointer::New(const SSL_METHOD* method) {
  return SSLCtxPointer(SSL_CTX_new(method));
}

bool SSLCtxPointer::setGroups(const char* groups) {
  return SSL_CTX_set1_groups_list(get(), groups) == 1;
}

bool SSLCtxPointer::setCipherSuites(const char* ciphers) {
  if (!ctx_) return false;
#ifndef OPENSSL_IS_BORINGSSL
  return SSL_CTX_set_ciphersuites(ctx_.get(), ciphers);
#elif defined(OPENSSL_IS_BORINGSSL)
  constexpr std::array<uint16_t, 3> supported = {
      SSL_CIPHER_AES_128_GCM_SHA256,
      SSL_CIPHER_AES_256_GCM_SHA384,
      SSL_CIPHER_CHACHA20_POLY1305_SHA256,
  };
  std::vector<uint16_t> cipher_ids;
  std::string_view remaining(ciphers);
  while (!remaining.empty()) {
    const size_t end = remaining.find(':');
    std::string_view name = remaining.substr(0, end);
    const size_t first = name.find_first_not_of(" \t\n\r\f\v");
    if (first != std::string_view::npos) {
      name = name.substr(first);
      name = name.substr(0, name.find_last_not_of(" \t\n\r\f\v") + 1);
      for (const auto id : supported) {
        const SSL_CIPHER* cipher = SSL_get_cipher_by_value(id);
        if (name == SSL_CIPHER_standard_name(cipher)) {
          // OpenSSL ignores unknown names and accepts duplicates, while
          // BoringSSL requires unique protocol IDs.
          if (std::find(cipher_ids.begin(), cipher_ids.end(), id) ==
              cipher_ids.end()) {
            cipher_ids.push_back(id);
          }
          break;
        }
      }
    }
    if (end == std::string_view::npos) break;
    remaining.remove_prefix(end + 1);
  }
  if (*ciphers != '\0' && cipher_ids.empty()) {
    OPENSSL_PUT_ERROR(SSL, SSL_R_NO_CIPHER_MATCH);
    return false;
  }
  std::vector<uint32_t> flags(cipher_ids.size());
  if (!flags.empty() &&
      (SSL_CTX_get_options(get()) & SSL_OP_CIPHER_SERVER_PREFERENCE) == 0) {
    // TLS 1.3 otherwise always uses server preference in BoringSSL. An
    // equal-preference group lets the client choose from the configured list.
    std::fill(flags.begin(),
              flags.end() - 1,
              SSL_CIPHER_FLAG_EQUAL_PREFERENCE_WITH_NEXT);
  }
  // An empty input resets BoringSSL's built-in preference order. The public
  // TLS API expands empty cipher strings to Node's defaults before this call.
  return SSL_CTX_set1_tls13_ciphers(
             get(), cipher_ids.data(), flags.data(), cipher_ids.size()) == 1;
#endif
}

// ============================================================================

namespace {
constexpr char AsciiToLower(char c) {
  return c >= 'A' && c <= 'Z' ? c + ('a' - 'A') : c;
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
constexpr auto kUnsupportedCipherFlags =
    EVP_CIPH_FLAG_CIPHER_WITH_MAC | EVP_CIPH_FLAG_TLS1_1_MULTIBLOCK;

bool HasUnsupportedCipherFlags(const EVP_CIPHER* cipher) {
  return (EVP_CIPHER_get_flags(cipher) & kUnsupportedCipherFlags) != 0;
}

bool IsSupportedLegacyCipher(const EVP_CIPHER* cipher) {
  return cipher != nullptr && cipher != EVP_enc_null() &&
         !HasUnsupportedCipherFlags(cipher);
}

bool IsSupportedFetchedCipher(const EVP_CIPHER* cipher) {
  if (cipher == nullptr || EVP_CIPHER_is_a(cipher, "NULL") ||
      HasUnsupportedCipherFlags(cipher)) {
    return false;
  }

#ifdef OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC
  int encrypt_then_mac = 0;
  OSSL_PARAM params[] = {
      OSSL_PARAM_construct_int(OSSL_CIPHER_PARAM_ENCRYPT_THEN_MAC,
                               &encrypt_then_mac),
      OSSL_PARAM_construct_end(),
  };
  if (EVP_CIPHER_get_params(const_cast<EVP_CIPHER*>(cipher), params) == 1 &&
      encrypt_then_mac != 0) {
    return false;
  }
#endif

  return true;
}

void PushAlgorithmAlias(const char* name, void* arg) {
  if (name == nullptr) return;
  static_cast<std::vector<std::string>*>(arg)->emplace_back(name);
}
#endif
}  // namespace

#if NCRYPTO_USE_OPENSSL_PROVIDER
Cipher::Cipher(DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> cipher)
    : cipher_(cipher.get()), fetched_cipher_(std::move(cipher)) {}
#endif

size_t CaseInsensitiveNameHash::operator()(
    std::string_view name) const noexcept {
  size_t hash = 5381;
  for (char c : name) hash = ((hash << 5) + hash) ^ AsciiToLower(c);
  return hash;
}

bool CaseInsensitiveNameEqual::operator()(std::string_view lhs,
                                          std::string_view rhs) const noexcept {
  if (lhs.size() != rhs.size()) return false;
  for (size_t n = 0; n < lhs.size(); n++) {
    if (AsciiToLower(lhs[n]) != AsciiToLower(rhs[n])) return false;
  }
  return true;
}

DigestCache::Result DigestCache::lookup(const char* name,
                                        uint64_t generation) const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (generation_ != generation) return {};
  const auto it = aliases_.find(name);
  if (it == aliases_.end()) return {};
  return lookup(it->second, generation);
#elif NCRYPTO_USE_BORINGSSL
  static_cast<void>(name);
  static_cast<void>(generation);
  return {};
#else
#error "Unsupported crypto backend"
#endif
}

DigestCache::Result DigestCache::insert(const char* name,
                                        const EVP_MD* digest,
                                        uint64_t generation) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (generation_ != generation || name == nullptr || digest == nullptr) {
    return {};
  }

  const char* canonical_name = EVP_MD_get0_name(digest);
  const OSSL_PROVIDER* provider = EVP_MD_get0_provider(digest);
  if (canonical_name == nullptr || provider == nullptr) return {};

  for (size_t index = 0; index < digests_.size(); index++) {
    const EVP_MD* cached = digests_[index].get();
    if (cached == nullptr) continue;
    const char* cached_name = EVP_MD_get0_name(cached);
    if (EVP_MD_get0_provider(cached) == provider && cached_name != nullptr &&
        CaseInsensitiveNameEqual()(cached_name, canonical_name)) {
      const int32_t id = static_cast<int32_t>(first_id_ + index);
      aliases_.insert_or_assign(name, id);
      return {cached, id};
    }
  }

  if (next_id_ == UINT32_MAX ||
      EVP_MD_up_ref(const_cast<EVP_MD*>(digest)) != 1) {
    return {};
  }

  digests_.emplace_back(const_cast<EVP_MD*>(digest));
  const int32_t id = static_cast<int32_t>(next_id_++);
  const size_t index = digests_.size() - 1;

  std::vector<std::string> aliases;
  EVP_MD_names_do_all(digests_[index].get(), PushAlgorithmAlias, &aliases);
  for (const std::string& alias : aliases) aliases_.emplace(alias, id);
  aliases_.insert_or_assign(name, id);

  return {digests_[index].get(), id};
#elif NCRYPTO_USE_BORINGSSL
  static_cast<void>(name);
  static_cast<void>(digest);
  static_cast<void>(generation);
  return {};
#else
#error "Unsupported crypto backend"
#endif
}

DigestCache::~DigestCache() = default;

void DigestCache::reset(uint64_t generation) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (generation_ == generation) return;
  aliases_.clear();
  hmac_templates_.clear();
  digests_.clear();
  first_id_ = next_id_;
#endif
  generation_ = generation;
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
const HMACCtxPointer* DigestCache::getHmacTemplate(const EVP_MD* digest,
                                                   uint64_t generation) {
  if (generation_ != generation || digest == nullptr) return nullptr;
  for (size_t index = 0; index < digests_.size(); index++) {
    if (digests_[index].get() != digest) continue;
    if (hmac_templates_.size() <= index) {
      hmac_templates_.resize(digests_.size());
    }
    if (!hmac_templates_[index]) {
      auto ctx = HMACCtxPointer::New(Digest::Ref(digest));
      if (!ctx) return nullptr;
      hmac_templates_[index] = std::make_unique<HMACCtxPointer>(std::move(ctx));
    }
    return hmac_templates_[index].get();
  }
  return nullptr;
}
#endif

const DigestCache::AliasMap& DigestCache::aliases() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return aliases_;
#elif NCRYPTO_USE_BORINGSSL
  static const AliasMap empty;
  return empty;
#else
#error "Unsupported crypto backend"
#endif
}

const EVP_CIPHER* CipherCache::lookup(const char* name, uint64_t generation) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (generation_ != generation) {
    aliases_.clear();
    ciphers_.clear();
    generation_ = generation;
  }

  const auto it = aliases_.find(name);
  if (it == aliases_.end()) return nullptr;
  if (it->second >= ciphers_.size()) return nullptr;
  return ciphers_[it->second].get();
#elif NCRYPTO_USE_BORINGSSL
  static_cast<void>(name);
  static_cast<void>(generation);
  return nullptr;
#else
#error "Unsupported crypto backend"
#endif
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
const EVP_CIPHER* CipherCache::insert(
    const char* name,
    DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free>&& cipher,
    uint64_t generation) {
  if (generation_ != generation || cipher == nullptr) return nullptr;

  const char* canonical_name = EVP_CIPHER_get0_name(cipher.get());
  const OSSL_PROVIDER* provider = EVP_CIPHER_get0_provider(cipher.get());
  if (canonical_name != nullptr && provider != nullptr) {
    for (size_t id = 0; id < ciphers_.size(); id++) {
      const EVP_CIPHER* cached = ciphers_[id].get();
      const char* cached_name = EVP_CIPHER_get0_name(cached);
      if (EVP_CIPHER_get0_provider(cached) == provider &&
          cached_name != nullptr &&
          CaseInsensitiveNameEqual()(cached_name, canonical_name)) {
        aliases_.insert_or_assign(name, id);
        return cached;
      }
    }
  }

  ciphers_.emplace_back(std::move(cipher));
  const size_t id = ciphers_.size() - 1;

  std::vector<std::string> aliases;
  EVP_CIPHER_names_do_all(ciphers_[id].get(), PushAlgorithmAlias, &aliases);
  for (const std::string& alias : aliases) {
    aliases_.emplace(alias, id);
  }
  aliases_.insert_or_assign(name, id);

  return ciphers_[id].get();
}
#endif

Cipher::Cipher(const Cipher& other) : cipher_(other.cipher_) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (other.fetched_cipher_ != nullptr) {
    if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) {
      fetched_cipher_.reset(other.fetched_cipher_.get());
    } else {
      cipher_ = nullptr;
    }
  }
#endif
}

Cipher& Cipher::operator=(const Cipher& other) {
  if (this == &other) return *this;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (other.fetched_cipher_ != nullptr) {
    if (EVP_CIPHER_up_ref(other.fetched_cipher_.get()) == 1) {
      fetched_cipher_.reset(other.fetched_cipher_.get());
    } else {
      fetched_cipher_.reset();
      cipher_ = nullptr;
      return *this;
    }
  } else {
    fetched_cipher_.reset();
  }
#endif
  cipher_ = other.cipher_;
  return *this;
}

const Cipher Cipher::FromName(const char* name, CipherCache* cache) {
  const EVP_CIPHER* cipher = EVP_get_cipherbyname(name);
  if (cipher != nullptr) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
    if (!IsSupportedLegacyCipher(cipher)) return Cipher();
#endif
    return Cipher(cipher);
  }

#if NCRYPTO_USE_OPENSSL_PROVIDER
  // A resolution that overlaps a FIPS transition may use either property
  // state. The cache retains the generation observed here, so the first
  // resolution begun after the transition clears any stale entries.
  const uint64_t generation = getFipsStateGeneration();
  if (cache != nullptr) {
    if (const EVP_CIPHER* cached = cache->lookup(name, generation)) {
      return Cipher(cached);
    }
  }

  MarkPopErrorOnReturn mark_pop_error_on_return;
  DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> fetched(
      EVP_CIPHER_fetch(nullptr, name, nullptr));
  if (!IsSupportedFetchedCipher(fetched.get())) return Cipher();

  if (cache != nullptr && generation == getFipsStateGeneration()) {
    if (const EVP_CIPHER* cached =
            cache->insert(name, std::move(fetched), generation)) {
      return Cipher(cached);
    }
  }

  return Cipher(std::move(fetched));
#elif NCRYPTO_USE_BORINGSSL
  static_cast<void>(cache);
  return Cipher();
#else
#error "Unsupported crypto backend"
#endif
}

const Cipher Cipher::FromNameForKeyEncoding(const char* name) {
  // Key serializers have their own cipher restrictions. Preserve their policy
  // instead of applying the filters used by the general cipher operations.
#if NCRYPTO_USE_OPENSSL_PROVIDER
  MarkPopErrorOnReturn mark_pop_error_on_return;
  DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> fetched(
      EVP_CIPHER_fetch(nullptr, name, nullptr));
  if (fetched) return Cipher(std::move(fetched));
#endif
  // Preserve serializer errors for known ciphers that cannot be fetched.
  return Cipher(EVP_get_cipherbyname(name));
}

const Cipher Cipher::FromNid(int nid, CipherCache* cache) {
  const EVP_CIPHER* cipher = EVP_get_cipherbynid(nid);
  if (cipher != nullptr) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
    if (!IsSupportedLegacyCipher(cipher)) return Cipher();
#endif
    return Cipher(cipher);
  }

#if NCRYPTO_USE_OPENSSL_PROVIDER
  const char* name = OBJ_nid2sn(nid);
  if (name != nullptr) return FromName(name, cache);
#elif NCRYPTO_USE_BORINGSSL
  static_cast<void>(cache);
#else
#error "Unsupported crypto backend"
#endif

  return Cipher();
}

const Cipher Cipher::FromCtx(const CipherCtxPointer& ctx) {
  return Cipher(GetCipherCtxCipher(ctx.get()));
}

namespace {
template <int nid>
const Cipher& GetPredefinedCipher() {
  static const Cipher cipher = Cipher::FromNid(nid);
  return cipher;
}
}  // namespace

const Cipher& Cipher::AES_128_CBC() {
  return GetPredefinedCipher<NID_aes_128_cbc>();
}

const Cipher& Cipher::AES_192_CBC() {
  return GetPredefinedCipher<NID_aes_192_cbc>();
}

const Cipher& Cipher::AES_256_CBC() {
  return GetPredefinedCipher<NID_aes_256_cbc>();
}

const Cipher& Cipher::AES_128_CTR() {
  return GetPredefinedCipher<NID_aes_128_ctr>();
}

const Cipher& Cipher::AES_192_CTR() {
  return GetPredefinedCipher<NID_aes_192_ctr>();
}

const Cipher& Cipher::AES_256_CTR() {
  return GetPredefinedCipher<NID_aes_256_ctr>();
}

const Cipher& Cipher::AES_128_GCM() {
  return GetPredefinedCipher<NID_aes_128_gcm>();
}

const Cipher& Cipher::AES_192_GCM() {
  return GetPredefinedCipher<NID_aes_192_gcm>();
}

const Cipher& Cipher::AES_256_GCM() {
  return GetPredefinedCipher<NID_aes_256_gcm>();
}

const Cipher& Cipher::AES_128_KW() {
  return GetPredefinedCipher<NID_id_aes128_wrap>();
}

const Cipher& Cipher::AES_192_KW() {
  return GetPredefinedCipher<NID_id_aes192_wrap>();
}

const Cipher& Cipher::AES_256_KW() {
  return GetPredefinedCipher<NID_id_aes256_wrap>();
}

#ifndef OPENSSL_IS_BORINGSSL
const Cipher& Cipher::AES_128_OCB() {
  return GetPredefinedCipher<NID_aes_128_ocb>();
}

const Cipher& Cipher::AES_192_OCB() {
  return GetPredefinedCipher<NID_aes_192_ocb>();
}

const Cipher& Cipher::AES_256_OCB() {
  return GetPredefinedCipher<NID_aes_256_ocb>();
}
#endif

const Cipher& Cipher::CHACHA20_POLY1305() {
  return GetPredefinedCipher<NID_chacha20_poly1305>();
}

bool Cipher::isGcmMode() const {
  if (!cipher_) return false;
  return getMode() == EVP_CIPH_GCM_MODE;
}

bool Cipher::isWrapMode() const {
  if (!cipher_) return false;
  return getMode() == EVP_CIPH_WRAP_MODE;
}

bool Cipher::isCtrMode() const {
  if (!cipher_) return false;
  return getMode() == EVP_CIPH_CTR_MODE;
}

bool Cipher::isCcmMode() const {
  if (!cipher_) return false;
  return getMode() == EVP_CIPH_CCM_MODE;
}

bool Cipher::isCtsMode() const {
  if (!cipher_) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return (EVP_CIPHER_get_flags(cipher_) & EVP_CIPH_FLAG_CTS) != 0;
#elif NCRYPTO_USE_BORINGSSL
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

bool Cipher::isOcbMode() const {
  if (!cipher_) return false;
  return getMode() == EVP_CIPH_OCB_MODE;
}

bool Cipher::isSivMode() const {
  if (!cipher_) return false;
#if OPENSSL_WITH_AES_SIV
  return getMode() == EVP_CIPH_SIV_MODE;
#elif NCRYPTO_USE_BORINGSSL
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

bool Cipher::isGcmSivMode() const {
  if (!cipher_) return false;
#if OPENSSL_WITH_AES_GCM_SIV
  return getMode() == EVP_CIPH_GCM_SIV_MODE;
#else
  return false;
#endif
}

bool Cipher::isStreamMode() const {
  if (!cipher_) return false;
  return getMode() == EVP_CIPH_STREAM_CIPHER;
}

bool Cipher::isChaCha20Poly1305() const {
  if (!cipher_) return false;
  return getNid() == NID_chacha20_poly1305;
}

int Cipher::getMode() const {
  if (!cipher_) return 0;
  return EVP_CIPHER_mode(cipher_);
}

int Cipher::getIvLength() const {
  if (!cipher_) return 0;
  return EVP_CIPHER_iv_length(cipher_);
}

int Cipher::getKeyLength() const {
  if (!cipher_) return 0;
  return EVP_CIPHER_key_length(cipher_);
}

int Cipher::getBlockSize() const {
  if (!cipher_) return 0;
  return EVP_CIPHER_block_size(cipher_);
}

int Cipher::getNid() const {
  if (!cipher_) return 0;
  return EVP_CIPHER_nid(cipher_);
}

std::string_view Cipher::getModeLabel() const {
  if (!cipher_) return {};
  switch (getMode()) {
    case EVP_CIPH_CCM_MODE:
      return "ccm";
    case EVP_CIPH_CFB_MODE:
      return "cfb";
    case EVP_CIPH_CBC_MODE:
      return "cbc";
    case EVP_CIPH_CTR_MODE:
      return "ctr";
    case EVP_CIPH_ECB_MODE:
      return "ecb";
    case EVP_CIPH_GCM_MODE:
      return "gcm";
    case EVP_CIPH_OCB_MODE:
      return "ocb";
    case EVP_CIPH_OFB_MODE:
      return "ofb";
#if OPENSSL_WITH_AES_SIV
    case EVP_CIPH_SIV_MODE:
      return "siv";
#endif
#if OPENSSL_WITH_AES_GCM_SIV
    case EVP_CIPH_GCM_SIV_MODE:
      return "gcm-siv";
#endif
    case EVP_CIPH_WRAP_MODE:
      return "wrap";
    case EVP_CIPH_XTS_MODE:
      return "xts";
    case EVP_CIPH_STREAM_CIPHER:
      return "stream";
  }
  return "{unknown}";
}

const char* Cipher::getName() const {
  if (!cipher_) return {};
  // OBJ_nid2sn(EVP_CIPHER_nid(cipher)) is used here instead of
  // EVP_CIPHER_name(cipher) for compatibility with BoringSSL.
  const int nid = getNid();
  if (nid != NID_undef) {
    const char* name = OBJ_nid2sn(nid);
    if (name != nullptr) return name;
  }
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return EVP_CIPHER_get0_name(cipher_);
#elif NCRYPTO_USE_BORINGSSL
  return {};
#else
#error "Unsupported crypto backend"
#endif
}

bool Cipher::isSupportedAuthenticatedMode() const {
  switch (getMode()) {
    case EVP_CIPH_CCM_MODE:
    case EVP_CIPH_GCM_MODE:
#ifndef OPENSSL_NO_OCB
    case EVP_CIPH_OCB_MODE:
#endif
#if OPENSSL_WITH_AES_SIV
    case EVP_CIPH_SIV_MODE:
#endif
#if OPENSSL_WITH_AES_GCM_SIV
    case EVP_CIPH_GCM_SIV_MODE:
#endif
      return true;
    case EVP_CIPH_STREAM_CIPHER:
      return getNid() == NID_chacha20_poly1305;
    default:
      return false;
  }
}

int Cipher::bytesToKey(const Digest& digest,
                       const Buffer<const unsigned char>& input,
                       unsigned char* key,
                       unsigned char* iv) const {
  return EVP_BytesToKey(
      *this, Digest::MD5, nullptr, input.data, input.len, 1, key, iv);
}

// ============================================================================

CipherCtxPointer CipherCtxPointer::New() {
  auto ret = CipherCtxPointer(EVP_CIPHER_CTX_new());
  if (!ret) return {};
  EVP_CIPHER_CTX_reset(ret.get());
  return ret;
}

CipherCtxPointer::CipherCtxPointer(EVP_CIPHER_CTX* ctx) : ctx_(ctx) {}

CipherCtxPointer::CipherCtxPointer(CipherCtxPointer&& other) noexcept
    : ctx_(other.release()) {}

CipherCtxPointer& CipherCtxPointer::operator=(
    CipherCtxPointer&& other) noexcept {
  if (this == &other) return *this;
  this->~CipherCtxPointer();
  return *new (this) CipherCtxPointer(std::move(other));
}

CipherCtxPointer::~CipherCtxPointer() {
  reset();
}

void CipherCtxPointer::reset(EVP_CIPHER_CTX* ctx) {
  ctx_.reset(ctx);
}

EVP_CIPHER_CTX* CipherCtxPointer::release() {
  return ctx_.release();
}

void CipherCtxPointer::setAllowWrap() {
  if (!ctx_) return;
  EVP_CIPHER_CTX_set_flags(ctx_.get(), EVP_CIPHER_CTX_FLAG_WRAP_ALLOW);
}

bool CipherCtxPointer::setKeyLength(size_t length) {
  if (!ctx_) return false;
  return EVP_CIPHER_CTX_set_key_length(ctx_.get(), length);
}

bool CipherCtxPointer::setIvLength(size_t length) {
  if (!ctx_) return false;
  return EVP_CIPHER_CTX_ctrl(
      ctx_.get(), EVP_CTRL_AEAD_SET_IVLEN, length, nullptr);
}

bool CipherCtxPointer::setAeadTag(const Buffer<const char>& tag) {
  if (!ctx_) return false;
  return EVP_CIPHER_CTX_ctrl(
      ctx_.get(), EVP_CTRL_AEAD_SET_TAG, tag.len, const_cast<char*>(tag.data));
}

bool CipherCtxPointer::setAeadTagLength(size_t length) {
  if (!ctx_) return false;
  return EVP_CIPHER_CTX_ctrl(
      ctx_.get(), EVP_CTRL_AEAD_SET_TAG, length, nullptr);
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
namespace {
// OSSL_CIPHER_PARAM_XTS_STANDARD is not defined by OpenSSL 3.0. Use its
// parameter name directly so custom providers can advertise it too.
constexpr char kCipherParamXtsStandard[] = "xts_standard";

bool SetCipherCtxStringParam(EVP_CIPHER_CTX* ctx,
                             const char* key,
                             const char* value) {
  if (ctx == nullptr || value == nullptr) return false;

  const OSSL_PARAM* settable = EVP_CIPHER_CTX_settable_params(ctx);
  const OSSL_PARAM* descriptor =
      settable == nullptr ? nullptr : OSSL_PARAM_locate_const(settable, key);
  if (descriptor == nullptr ||
      descriptor->data_type != OSSL_PARAM_UTF8_STRING) {
    return false;
  }

  OSSL_PARAM params[] = {
      OSSL_PARAM_construct_utf8_string(key, const_cast<char*>(value), 0),
      OSSL_PARAM_END,
  };
  return EVP_CIPHER_CTX_set_params(ctx, params) == 1;
}
}  // namespace
#endif

bool CipherCtxPointer::setCtsMode(const char* mode) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return SetCipherCtxStringParam(ctx_.get(), OSSL_CIPHER_PARAM_CTS_MODE, mode);
#elif NCRYPTO_USE_BORINGSSL
  static_cast<void>(mode);
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

bool CipherCtxPointer::setPadding(bool padding) {
  if (!ctx_) return false;
  return EVP_CIPHER_CTX_set_padding(ctx_.get(), padding);
}

bool CipherCtxPointer::setXtsStandard(const char* standard) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return SetCipherCtxStringParam(ctx_.get(), kCipherParamXtsStandard, standard);
#elif NCRYPTO_USE_BORINGSSL
  static_cast<void>(standard);
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

int CipherCtxPointer::getBlockSize() const {
  if (!ctx_) return 0;
  return EVP_CIPHER_CTX_block_size(ctx_.get());
}

int CipherCtxPointer::getMode() const {
  if (!ctx_) return 0;
  return EVP_CIPHER_CTX_mode(ctx_.get());
}

bool CipherCtxPointer::isGcmMode() const {
  if (!ctx_) return false;
  return getMode() == EVP_CIPH_GCM_MODE;
}

bool CipherCtxPointer::isOcbMode() const {
  if (!ctx_) return false;
  return getMode() == EVP_CIPH_OCB_MODE;
}

bool CipherCtxPointer::isCcmMode() const {
  if (!ctx_) return false;
  return getMode() == EVP_CIPH_CCM_MODE;
}

bool CipherCtxPointer::isCtsMode() const {
  if (!ctx_) return false;
  return Cipher::FromCtx(*this).isCtsMode();
}

bool CipherCtxPointer::isXtsMode() const {
  if (!ctx_) return false;
  return getMode() == EVP_CIPH_XTS_MODE;
}

bool CipherCtxPointer::isWrapMode() const {
  if (!ctx_) return false;
  return getMode() == EVP_CIPH_WRAP_MODE;
}

bool CipherCtxPointer::isSivMode() const {
  if (!ctx_) return false;
#if OPENSSL_WITH_AES_SIV
  return getMode() == EVP_CIPH_SIV_MODE;
#elif NCRYPTO_USE_BORINGSSL
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

bool CipherCtxPointer::isGcmSivMode() const {
  if (!ctx_) return false;
#if OPENSSL_WITH_AES_GCM_SIV
  return getMode() == EVP_CIPH_GCM_SIV_MODE;
#else
  return false;
#endif
}

bool CipherCtxPointer::isChaCha20Poly1305() const {
  if (!ctx_) return false;
  return getNid() == NID_chacha20_poly1305;
}

int CipherCtxPointer::getNid() const {
  if (!ctx_) return 0;
  return EVP_CIPHER_CTX_nid(ctx_.get());
}

bool CipherCtxPointer::init(const Cipher& cipher,
                            bool encrypt,
                            const unsigned char* key,
                            const unsigned char* iv) {
  if (!ctx_) return false;
  return EVP_CipherInit_ex(
             ctx_.get(), cipher, nullptr, key, iv, encrypt ? 1 : 0) == 1;
}

bool CipherCtxPointer::update(const Buffer<const unsigned char>& in,
                              unsigned char* out,
                              int* out_len,
                              bool finalize) {
  if (!ctx_) return false;
  if (!finalize) {
    return EVP_CipherUpdate(ctx_.get(), out, out_len, in.data, in.len) == 1;
  }
  return EVP_CipherFinal_ex(ctx_.get(), out, out_len) == 1;
}

bool CipherCtxPointer::getAeadTag(size_t len, unsigned char* out) {
  if (!ctx_) return false;
  return EVP_CIPHER_CTX_ctrl(ctx_.get(), EVP_CTRL_AEAD_GET_TAG, len, out);
}

// ============================================================================

ECDSASigPointer::ECDSASigPointer() : sig_(nullptr) {}
ECDSASigPointer::ECDSASigPointer(ECDSA_SIG* sig) : sig_(sig) {
  if (sig_) {
    ECDSA_SIG_get0(sig_.get(), &pr_, &ps_);
  }
}
ECDSASigPointer::ECDSASigPointer(ECDSASigPointer&& other) noexcept
    : sig_(other.release()) {
  if (sig_) {
    ECDSA_SIG_get0(sig_.get(), &pr_, &ps_);
  }
}

ECDSASigPointer& ECDSASigPointer::operator=(ECDSASigPointer&& other) noexcept {
  sig_.reset(other.release());
  if (sig_) {
    ECDSA_SIG_get0(sig_.get(), &pr_, &ps_);
  }
  return *this;
}

ECDSASigPointer::~ECDSASigPointer() {
  reset();
}

void ECDSASigPointer::reset(ECDSA_SIG* sig) {
  sig_.reset();
  pr_ = nullptr;
  ps_ = nullptr;
}

ECDSA_SIG* ECDSASigPointer::release() {
  pr_ = nullptr;
  ps_ = nullptr;
  return sig_.release();
}

ECDSASigPointer ECDSASigPointer::New() {
  return ECDSASigPointer(ECDSA_SIG_new());
}

ECDSASigPointer ECDSASigPointer::Parse(const Buffer<const unsigned char>& sig) {
  const unsigned char* ptr = sig.data;
  return ECDSASigPointer(d2i_ECDSA_SIG(nullptr, &ptr, sig.len));
}

bool ECDSASigPointer::setParams(BignumPointer&& r, BignumPointer&& s) {
  if (!sig_) return false;
  return ECDSA_SIG_set0(sig_.get(), r.release(), s.release());
}

Buffer<unsigned char> ECDSASigPointer::encode() const {
  if (!sig_)
    return {
        .data = nullptr,
        .len = 0,
    };
  Buffer<unsigned char> buf;
  buf.len = i2d_ECDSA_SIG(sig_.get(), &buf.data);
  return buf;
}

// ============================================================================

ECGroupPointer::ECGroupPointer() : group_(nullptr) {}

ECGroupPointer::ECGroupPointer(EC_GROUP* group) : group_(group) {}

ECGroupPointer::ECGroupPointer(ECGroupPointer&& other) noexcept
    : group_(other.release()) {}

ECGroupPointer& ECGroupPointer::operator=(ECGroupPointer&& other) noexcept {
  group_.reset(other.release());
  return *this;
}

ECGroupPointer::~ECGroupPointer() {
  reset();
}

void ECGroupPointer::reset(EC_GROUP* group) {
  group_.reset();
}

EC_GROUP* ECGroupPointer::release() {
  return group_.release();
}

ECGroupPointer ECGroupPointer::NewByCurveName(int nid) {
  return ECGroupPointer(EC_GROUP_new_by_curve_name(nid));
}

// ============================================================================

ECPointPointer::ECPointPointer() : point_(nullptr) {}

ECPointPointer::ECPointPointer(EC_POINT* point) : point_(point) {}

ECPointPointer::ECPointPointer(ECPointPointer&& other) noexcept
    : point_(other.release()) {}

ECPointPointer& ECPointPointer::operator=(ECPointPointer&& other) noexcept {
  point_.reset(other.release());
  return *this;
}

ECPointPointer::~ECPointPointer() {
  reset();
}

void ECPointPointer::reset(EC_POINT* point) {
  point_.reset(point);
}

EC_POINT* ECPointPointer::release() {
  return point_.release();
}

ECPointPointer ECPointPointer::New(const EC_GROUP* group) {
  return ECPointPointer(EC_POINT_new(group));
}

bool ECPointPointer::setFromBuffer(const Buffer<const unsigned char>& buffer,
                                   const EC_GROUP* group) {
  if (!point_) return false;
  return EC_POINT_oct2point(
      group, point_.get(), buffer.data, buffer.len, nullptr);
}

bool ECPointPointer::mul(const EC_GROUP* group, const BIGNUM* priv_key) {
  if (!point_) return false;
  return EC_POINT_mul(group, point_.get(), priv_key, nullptr, nullptr, nullptr);
}

// ============================================================================

bool ECKeyPointer::checkPrivateKey() const {
  const auto group = getGroup();
  const auto priv = getPrivateKey();
  const auto pub = getPublicKey();
  if (group == nullptr || priv == nullptr || pub == nullptr) return false;

  auto order = BignumPointer::New();
  if (!order || !EC_GROUP_get_order(group, order.get(), nullptr) ||
      BN_is_zero(priv) || BN_is_negative(priv) ||
      BN_cmp(priv, order.get()) >= 0) {
    return false;
  }

  auto expected = ECPointPointer::New(group);
  return expected && expected.mul(group, priv) &&
         EC_POINT_cmp(group, expected.get(), pub, nullptr) == 0;
}

#if NCRYPTO_USE_BORINGSSL
ECKeyPointer::ECKeyPointer() : key_(nullptr) {}

ECKeyPointer::ECKeyPointer(const EVPKeyPointer& key) : key_(nullptr) {
  if (!key.isA(KeyAlgorithm::EC)) return;
  const EC_KEY* ec = key;
  if (ec != nullptr) key_.reset(EC_KEY_dup(ec));
}

ECKeyPointer::ECKeyPointer(EC_KEY* key) : key_(key) {}

ECKeyPointer::ECKeyPointer(ECKeyPointer&& other) noexcept
    : key_(other.release()) {}

ECKeyPointer& ECKeyPointer::operator=(ECKeyPointer&& other) noexcept {
  key_.reset(other.release());
  return *this;
}

ECKeyPointer::~ECKeyPointer() {
  reset();
}

void ECKeyPointer::reset(EC_KEY* key) {
  key_.reset(key);
}

EC_KEY* ECKeyPointer::release() {
  return key_.release();
}

ECKeyPointer ECKeyPointer::clone() const {
  if (!key_) return {};
  return ECKeyPointer(EC_KEY_dup(key_.get()));
}

bool ECKeyPointer::generate() {
  if (!key_) return false;
  return EC_KEY_generate_key(key_.get());
}

bool ECKeyPointer::setPublicKey(const ECPointPointer& pub) {
  if (!key_) return false;
  return EC_KEY_set_public_key(key_.get(), pub.get()) == 1;
}

bool ECKeyPointer::setPublicKeyRaw(const BignumPointer& x,
                                   const BignumPointer& y) {
  if (!key_) return false;
  const EC_GROUP* group = EC_KEY_get0_group(key_.get());
  if (group == nullptr) return false;

  // For curves with cofactor h=1, use EC_POINT_oct2point +
  // EC_KEY_set_public_key instead of EC_KEY_set_public_key_affine_coordinates.
  // The latter internally calls EC_KEY_check_key() which performs a scalar
  // multiplication (n*Q) for order validation — redundant when h=1 since every
  // on-curve point already has order n. EC_POINT_oct2point validates the point
  // is on the curve, which is sufficient. For curves with h!=1, fall back to
  // the full check.
  auto cofactor = BignumPointer::New();
  if (!cofactor || !EC_GROUP_get_cofactor(group, cofactor.get(), nullptr) ||
      !cofactor.isOne()) {
    return EC_KEY_set_public_key_affine_coordinates(
               key_.get(), x.get(), y.get()) == 1;
  }

  // Field element byte length: ceil(degree_bits / 8).
  size_t field_len = (EC_GROUP_get_degree(group) + 7) / 8;
  // Build an uncompressed point: 0x04 || x || y, each padded to field_len.
  size_t uncompressed_len = 1 + 2 * field_len;
  auto buf = DataPointer::Alloc(uncompressed_len);
  if (!buf) return false;
  unsigned char* ptr = static_cast<unsigned char*>(buf.get());
  ptr[0] = POINT_CONVERSION_UNCOMPRESSED;
  if (x.encodePaddedInto(ptr + 1, field_len) != field_len ||
      y.encodePaddedInto(ptr + 1 + field_len, field_len) != field_len) {
    return false;
  }

  auto point = ECPointPointer::New(group);
  if (!point) return false;
  if (!point.setFromBuffer({ptr, uncompressed_len}, group)) return false;
  return EC_KEY_set_public_key(key_.get(), point.get()) == 1;
}

bool ECKeyPointer::setPrivateKey(const BignumPointer& priv) {
  if (!key_) return false;
  return EC_KEY_set_private_key(key_.get(), priv.get()) == 1;
}

const BIGNUM* ECKeyPointer::getPrivateKey() const {
  if (!key_) return nullptr;
  return GetPrivateKey(key_.get());
}

const BIGNUM* ECKeyPointer::GetPrivateKey(const EC_KEY* key) {
  return EC_KEY_get0_private_key(key);
}

const EC_POINT* ECKeyPointer::getPublicKey() const {
  if (!key_) return nullptr;
  return GetPublicKey(key_.get());
}

const EC_POINT* ECKeyPointer::GetPublicKey(const EC_KEY* key) {
  return EC_KEY_get0_public_key(key);
}

const EC_GROUP* ECKeyPointer::getGroup() const {
  if (!key_) return nullptr;
  return GetGroup(key_.get());
}

const EC_GROUP* ECKeyPointer::GetGroup(const EC_KEY* key) {
  return EC_KEY_get0_group(key);
}

int ECKeyPointer::GetGroupName(const EC_KEY* key) {
  const EC_GROUP* group = GetGroup(key);
  return group ? EC_GROUP_get_curve_name(group) : 0;
}

bool ECKeyPointer::Check(const EC_KEY* key) {
  return EC_KEY_check_key(key) == 1;
}

bool ECKeyPointer::checkKey() const {
  if (!key_) return false;
  return Check(key_.get());
}

DataPointer ECKeyPointer::computeSecret(const ECPointPointer& peer) const {
  if (!key_ || !peer) return {};
  const EC_GROUP* group = getGroup();
  const int field_size = EC_GROUP_get_degree(group);
  const size_t out_len = (field_size + 7) / 8;
  auto out = DataPointer::Alloc(out_len);
  if (!out) return {};
  if (ECDH_compute_key(
          out.get(), out.size(), peer.get(), key_.get(), nullptr) == 0) {
    return {};
  }
  return out;
}

ECKeyPointer ECKeyPointer::NewByCurveName(int nid) {
  return ECKeyPointer(EC_KEY_new_by_curve_name(nid));
}

ECKeyPointer ECKeyPointer::New(const EC_GROUP* group) {
  auto ptr = ECKeyPointer(EC_KEY_new());
  if (!ptr) return {};
  if (!EC_KEY_set_group(ptr.get(), group)) return {};
  return ptr;
}
#elif NCRYPTO_USE_OPENSSL_PROVIDER
ECKeyPointer::ECKeyPointer() : group_(nullptr), pub_(nullptr), priv_(nullptr) {}

ECKeyPointer::ECKeyPointer(const EVPKeyPointer& key) : ECKeyPointer() {
  if (!key.isA(KeyAlgorithm::EC)) return;
  char group_name[80];
  size_t group_name_len = 0;
  if (EVP_PKEY_get_utf8_string_param(key.get(),
                                     OSSL_PKEY_PARAM_GROUP_NAME,
                                     group_name,
                                     sizeof(group_name),
                                     &group_name_len) != 1) {
    return;
  }

  const int nid = Ec::GetCurveIdFromName(group_name);
  if (nid == NID_undef) return;
  group_.reset(EC_GROUP_new_by_curve_name(nid));
  if (!group_) return;

  GetOptionalPKeyBnParam(key.get(), OSSL_PKEY_PARAM_PRIV_KEY, &priv_);

  size_t public_key_len = 0;
  if (EVP_PKEY_get_octet_string_param(
          key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &public_key_len) ==
      1) {
    auto public_key = DataPointer::Alloc(public_key_len);
    if (!public_key || EVP_PKEY_get_octet_string_param(
                           key.get(),
                           OSSL_PKEY_PARAM_PUB_KEY,
                           static_cast<unsigned char*>(public_key.get()),
                           public_key.size(),
                           &public_key_len) != 1) {
      reset();
      return;
    }

    auto point = ECPointPointer::New(group_.get());
    if (!point ||
        !point.setFromBuffer(
            {static_cast<unsigned char*>(public_key.get()), public_key_len},
            group_.get())) {
      reset();
      return;
    }
    pub_.reset(point.release());
  }
}

ECKeyPointer::ECKeyPointer(ECKeyPointer&& other) noexcept
    : group_(std::move(other.group_)),
      pub_(std::move(other.pub_)),
      priv_(std::move(other.priv_)) {}

ECKeyPointer& ECKeyPointer::operator=(ECKeyPointer&& other) noexcept {
  group_ = std::move(other.group_);
  pub_ = std::move(other.pub_);
  priv_ = std::move(other.priv_);
  return *this;
}

ECKeyPointer::~ECKeyPointer() {
  reset();
}

void ECKeyPointer::reset() {
  group_.reset();
  pub_.reset();
  priv_.reset();
}

ECKeyPointer ECKeyPointer::clone() const {
  if (!group_) return {};
  ECKeyPointer ret;
  ret.group_.reset(EC_GROUP_dup(group_.get()));
  if (!ret.group_) return {};
  if (pub_ != nullptr) {
    ret.pub_.reset(EC_POINT_dup(pub_.get(), ret.group_.get()));
    if (!ret.pub_) return {};
  }
  if (priv_ != nullptr) {
    ret.priv_.reset(BN_dup(priv_.get()));
    if (!ret.priv_) return {};
  }
  return ret;
}

bool ECKeyPointer::generate() {
  if (!group_) return false;
  const int nid = EC_GROUP_get_curve_name(group_.get());
  auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::EC);
  if (!ctx || !ctx.initForKeygen() ||
      !ctx.setEcParameters(nid, OPENSSL_EC_NAMED_CURVE)) {
    return false;
  }

  EVP_PKEY* raw = nullptr;
  if (EVP_PKEY_keygen(ctx.get(), &raw) != 1) return false;
  EVPKeyPointer pkey(raw);

  DeleteFnPtr<BIGNUM, BN_clear_free> priv;
  if (!GetPKeyBnParam(pkey.get(), OSSL_PKEY_PARAM_PRIV_KEY, &priv)) {
    return false;
  }

  size_t public_key_len = 0;
  if (EVP_PKEY_get_octet_string_param(
          pkey.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &public_key_len) !=
      1) {
    return false;
  }

  auto public_key = DataPointer::Alloc(public_key_len);
  if (!public_key || EVP_PKEY_get_octet_string_param(
                         pkey.get(),
                         OSSL_PKEY_PARAM_PUB_KEY,
                         static_cast<unsigned char*>(public_key.get()),
                         public_key.size(),
                         &public_key_len) != 1) {
    return false;
  }

  auto point = ECPointPointer::New(group_.get());
  if (!point ||
      !point.setFromBuffer(
          {static_cast<unsigned char*>(public_key.get()), public_key_len},
          group_.get())) {
    return false;
  }

  priv_ = std::move(priv);
  pub_.reset(point.release());
  return true;
}

bool ECKeyPointer::setPublicKey(const ECPointPointer& pub) {
  if (!group_ || !pub) return false;
  pub_.reset(EC_POINT_dup(pub.get(), group_.get()));
  return pub_ != nullptr;
}

bool ECKeyPointer::setPublicKeyRaw(const BignumPointer& x,
                                   const BignumPointer& y) {
  if (!group_ || !x || !y) return false;
  const size_t field_len = (EC_GROUP_get_degree(group_.get()) + 7) / 8;
  const size_t uncompressed_len = 1 + 2 * field_len;
  auto buf = DataPointer::Alloc(uncompressed_len);
  if (!buf) return false;
  unsigned char* ptr = static_cast<unsigned char*>(buf.get());
  ptr[0] = POINT_CONVERSION_UNCOMPRESSED;
  if (x.encodePaddedInto(ptr + 1, field_len) != field_len ||
      y.encodePaddedInto(ptr + 1 + field_len, field_len) != field_len) {
    return false;
  }

  auto point = ECPointPointer::New(group_.get());
  if (!point || !point.setFromBuffer({ptr, uncompressed_len}, group_.get())) {
    return false;
  }
  pub_.reset(point.release());
  return true;
}

bool ECKeyPointer::setPrivateKey(const BignumPointer& priv) {
  if (!group_ || !priv) return false;
  priv_.reset(BN_dup(priv.get()));
  return priv_ != nullptr;
}

const BIGNUM* ECKeyPointer::getPrivateKey() const {
  return priv_.get();
}

const EC_POINT* ECKeyPointer::getPublicKey() const {
  return pub_.get();
}

const EC_GROUP* ECKeyPointer::getGroup() const {
  return group_.get();
}

bool ECKeyPointer::checkKey() const {
  if (!group_) return false;

  if (priv_ != nullptr) {
    auto order = BignumPointer::New();
    if (!order || !EC_GROUP_get_order(group_.get(), order.get(), nullptr)) {
      return false;
    }
    if (BN_is_zero(priv_.get()) || BN_is_negative(priv_.get()) ||
        BN_cmp(priv_.get(), order.get()) >= 0) {
      return false;
    }
  }

  if (pub_ != nullptr &&
      EC_POINT_is_on_curve(group_.get(), pub_.get(), nullptr) != 1) {
    return false;
  }

  if (priv_ != nullptr && pub_ != nullptr) {
    auto expected = ECPointPointer::New(group_.get());
    if (!expected || !expected.mul(group_.get(), priv_.get()) ||
        EC_POINT_cmp(group_.get(), expected.get(), pub_.get(), nullptr) != 0) {
      return false;
    }
  }

  auto pkey = EVPKeyPointer::New();
  if (!pkey || !pkey.set(*this)) return false;
  EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(pkey.get(), nullptr));
  if (!ctx) return false;
  if (pub_ != nullptr && EVP_PKEY_public_check(ctx.get()) != 1) return false;
  if (priv_ != nullptr && EVP_PKEY_private_check(ctx.get()) != 1) return false;
  return true;
}

DataPointer ECKeyPointer::computeSecret(const ECPointPointer& peer) const {
  if (!group_ || !priv_ || !peer) return {};
  auto our_key = EVPKeyPointer::New();
  auto their_key = EVPKeyPointer::New();
  auto their_ec = ECKeyPointer::New(group_.get());
  if (!our_key || !their_key || !our_key.set(*this) ||
      !their_ec.setPublicKey(peer) || !their_key.set(their_ec)) {
    return {};
  }

  EVPKeyCtxPointer ctx(EVP_PKEY_CTX_new(our_key.get(), nullptr));
  size_t out_len = 0;
  if (!ctx || EVP_PKEY_derive_init(ctx.get()) != 1 ||
      EVP_PKEY_derive_set_peer(ctx.get(), their_key.get()) != 1 ||
      EVP_PKEY_derive(ctx.get(), nullptr, &out_len) != 1) {
    return {};
  }

  auto out = DataPointer::Alloc(out_len);
  if (!out) return {};
  if (EVP_PKEY_derive(
          ctx.get(), static_cast<unsigned char*>(out.get()), &out_len) != 1) {
    return {};
  }
  return out.resize(out_len);
}

ECKeyPointer ECKeyPointer::NewByCurveName(int nid) {
  ECKeyPointer ret;
  ret.group_.reset(EC_GROUP_new_by_curve_name(nid));
  return ret;
}

ECKeyPointer ECKeyPointer::New(const EC_GROUP* group) {
  ECKeyPointer ret;
  if (group != nullptr) {
    ret.group_.reset(EC_GROUP_dup(group));
  }
  return ret;
}
#else
#error "Unsupported crypto backend"
#endif  // NCRYPTO_USE_BORINGSSL

// ============================================================================

EVPKeyCtxPointer::EVPKeyCtxPointer() : ctx_(nullptr) {}

EVPKeyCtxPointer::EVPKeyCtxPointer(EVP_PKEY_CTX* ctx) : ctx_(ctx) {}

EVPKeyCtxPointer::EVPKeyCtxPointer(EVPKeyCtxPointer&& other) noexcept
    : ctx_(other.release()) {}

EVPKeyCtxPointer& EVPKeyCtxPointer::operator=(
    EVPKeyCtxPointer&& other) noexcept {
  ctx_.reset(other.release());
  return *this;
}

EVPKeyCtxPointer::~EVPKeyCtxPointer() {
  reset();
}

void EVPKeyCtxPointer::reset(EVP_PKEY_CTX* ctx) {
  ctx_.reset(ctx);
}

EVP_PKEY_CTX* EVPKeyCtxPointer::release() {
  return ctx_.release();
}

EVPKeyCtxPointer EVPKeyCtxPointer::New(const EVPKeyPointer& key) {
  if (!key) return {};
  return EVPKeyCtxPointer(EVP_PKEY_CTX_new(key.get(), nullptr));
}

EVPKeyCtxPointer EVPKeyCtxPointer::NewFromName(const char* name) {
  if (name == nullptr) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return EVPKeyCtxPointer(EVP_PKEY_CTX_new_from_name(nullptr, name, nullptr));
#elif NCRYPTO_USE_BORINGSSL
  const int id = GetBoringSSLKeyId(name);
  if (id == NID_undef) return {};
  // DSA keys are not supported with BoringSSL.
  if (id == EVP_PKEY_DSA) return {};
  return EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(id, nullptr));
#else
#error "Unsupported crypto backend"
#endif
}

EVPKeyCtxPointer EVPKeyCtxPointer::NewFromAlgorithm(
    const KeyAlgorithm& algorithm) {
  return NewFromName(algorithm.name());
}

bool EVPKeyCtxPointer::initForDerive(const EVPKeyPointer& peer) {
  if (!ctx_) return false;
  if (EVP_PKEY_derive_init(ctx_.get()) != 1) return false;
  return EVP_PKEY_derive_set_peer(ctx_.get(), peer.get()) == 1;
}

bool EVPKeyCtxPointer::initForKeygen() {
  if (!ctx_) return false;
  return EVP_PKEY_keygen_init(ctx_.get()) == 1;
}

bool EVPKeyCtxPointer::initForParamgen() {
  if (!ctx_) return false;
  return EVP_PKEY_paramgen_init(ctx_.get()) == 1;
}

int EVPKeyCtxPointer::initForVerify() {
  if (!ctx_) return 0;
  return EVP_PKEY_verify_init(ctx_.get());
}

int EVPKeyCtxPointer::initForSign() {
  if (!ctx_) return 0;
  return EVP_PKEY_sign_init(ctx_.get());
}

bool EVPKeyCtxPointer::setDhParameters(int prime_size, uint32_t generator) {
#ifndef OPENSSL_IS_BORINGSSL
  if (!ctx_) return false;
  return EVP_PKEY_CTX_set_dh_paramgen_prime_len(ctx_.get(), prime_size) == 1 &&
         EVP_PKEY_CTX_set_dh_paramgen_generator(ctx_.get(), generator) == 1;
#elif defined(OPENSSL_IS_BORINGSSL)
  // TODO(jasnell): Boringssl appears not to support this operation.
  // Is there an alternative approach that Boringssl does support?
  return false;
#endif
}

bool EVPKeyCtxPointer::setDsaParameters(uint32_t bits,
                                        std::optional<int> q_bits) {
  if (!ctx_) return false;
  if (EVP_PKEY_CTX_set_dsa_paramgen_bits(ctx_.get(), bits) != 1) {
    return false;
  }
  if (q_bits.has_value() &&
      EVP_PKEY_CTX_set_dsa_paramgen_q_bits(ctx_.get(), q_bits.value()) != 1) {
    return false;
  }
  return true;
}

bool EVPKeyCtxPointer::setEcParameters(int curve, int encoding) {
  return setEcParameters(OBJ_nid2sn(curve), encoding);
}

bool EVPKeyCtxPointer::setEcParameters(const char* group_name, int encoding) {
  if (!ctx_ || group_name == nullptr) return false;
  const int curve = Ec::GetCurveIdFromName(group_name);
#if NCRYPTO_USE_OPENSSL_PROVIDER
  // Keep the historical aliases while allowing names known only to providers.
  if (curve != NID_undef) group_name = OBJ_nid2sn(curve);

  const char* encoding_name = nullptr;
  switch (encoding) {
    case OPENSSL_EC_EXPLICIT_CURVE:
      encoding_name = OSSL_PKEY_EC_ENCODING_EXPLICIT;
      break;
    case OPENSSL_EC_NAMED_CURVE:
      encoding_name = OSSL_PKEY_EC_ENCODING_GROUP;
      break;
    default:
      return false;
  }
  OSSL_PARAM params[] = {
      OSSL_PARAM_construct_utf8_string(
          OSSL_PKEY_PARAM_GROUP_NAME, const_cast<char*>(group_name), 0),
      OSSL_PARAM_construct_utf8_string(
          OSSL_PKEY_PARAM_EC_ENCODING, const_cast<char*>(encoding_name), 0),
      OSSL_PARAM_END,
  };
  return EVP_PKEY_CTX_set_params(ctx_.get(), params) == 1;
#elif NCRYPTO_USE_BORINGSSL
  return curve != NID_undef &&
         EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx_.get(), curve) == 1 &&
         EVP_PKEY_CTX_set_ec_param_enc(ctx_.get(), encoding) == 1;
#else
#error "Unsupported crypto backend"
#endif
}

bool EVPKeyCtxPointer::setRsaOaepMd(const Digest& md) {
  if (!md || !ctx_) return false;
  const EVP_MD* md_ptr = md;
  return EVP_PKEY_CTX_set_rsa_oaep_md(ctx_.get(), md_ptr) > 0;
}

bool EVPKeyCtxPointer::setRsaMgf1Md(const Digest& md) {
  if (!md || !ctx_) return false;
  const EVP_MD* md_ptr = md;
  return EVP_PKEY_CTX_set_rsa_mgf1_md(ctx_.get(), md_ptr) > 0;
}

bool EVPKeyCtxPointer::setRsaPadding(int padding) {
  return setRsaPadding(ctx_.get(), padding, std::nullopt);
}

bool EVPKeyCtxPointer::setRsaPadding(EVP_PKEY_CTX* ctx,
                                     int padding,
                                     std::optional<int> salt_len) {
  if (ctx == nullptr) return false;
  if (EVP_PKEY_CTX_set_rsa_padding(ctx, padding) <= 0) {
    return false;
  }
  if (padding == RSA_PKCS1_PSS_PADDING && salt_len.has_value()) {
    return EVP_PKEY_CTX_set_rsa_pss_saltlen(ctx, salt_len.value()) > 0;
  }
  return true;
}

bool EVPKeyCtxPointer::setRsaKeygenBits(int bits) {
  if (!ctx_) return false;
  return EVP_PKEY_CTX_set_rsa_keygen_bits(ctx_.get(), bits) == 1;
}

bool EVPKeyCtxPointer::setRsaKeygenPubExp(BignumPointer&& e) {
  if (!ctx_) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return EVP_PKEY_CTX_set1_rsa_keygen_pubexp(ctx_.get(), e.get()) == 1;
#elif NCRYPTO_USE_BORINGSSL
  if (EVP_PKEY_CTX_set_rsa_keygen_pubexp(ctx_.get(), e.get()) == 1) {
    // The ctx_ takes ownership of e on success.
    e.release();
    return true;
  }
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

bool EVPKeyCtxPointer::setRsaPssKeygenMd(const Digest& md) {
  if (!md || !ctx_) return false;
  return EVP_PKEY_CTX_set_rsa_pss_keygen_md(ctx_.get(), md) > 0;
}

bool EVPKeyCtxPointer::setRsaPssKeygenMgf1Md(const Digest& md) {
  if (!md || !ctx_) return false;
  return EVP_PKEY_CTX_set_rsa_pss_keygen_mgf1_md(ctx_.get(), md) > 0;
}

bool EVPKeyCtxPointer::setRsaPssSaltlen(int salt_len) {
  if (!ctx_) return false;
  return EVP_PKEY_CTX_set_rsa_pss_keygen_saltlen(ctx_.get(), salt_len) > 0;
}

bool EVPKeyCtxPointer::setRsaImplicitRejection() {
#ifndef OPENSSL_IS_BORINGSSL
  if (!ctx_) return false;
  return EVP_PKEY_CTX_ctrl_str(
             ctx_.get(), "rsa_pkcs1_implicit_rejection", "1") > 0;
  // From the doc -2 means that the option is not supported.
  // The default for the option is enabled and if it has been
  // specifically disabled we want to respect that so we will
  // not throw an error if the option is supported regardless
  // of how it is set. The call to set the value
  // will not affect what is used since a different context is
  // used in the call if the option is supported
#elif defined(OPENSSL_IS_BORINGSSL)
  // TODO(jasnell): Boringssl appears not to support this operation.
  // Is there an alternative approach that Boringssl does support?
  return true;
#endif
}

bool EVPKeyCtxPointer::setRsaOaepLabel(DataPointer&& data) {
  if (!ctx_) return false;
  if (EVP_PKEY_CTX_set0_rsa_oaep_label(ctx_.get(),
                                       static_cast<unsigned char*>(data.get()),
                                       data.size()) > 0) {
    // The ctx_ takes ownership of data on success.
    data.release();
    return true;
  }
  return false;
}

bool EVPKeyCtxPointer::setSignatureMd(const EVPMDCtxPointer& md) {
  if (!ctx_) return false;
  return EVP_PKEY_CTX_set_signature_md(ctx_.get(), GetDigestCtxMd(md.get())) ==
         1;
}

bool EVPKeyCtxPointer::initForEncrypt() {
  if (!ctx_) return false;
  return EVP_PKEY_encrypt_init(ctx_.get()) == 1;
}

bool EVPKeyCtxPointer::initForDecrypt() {
  if (!ctx_) return false;
  return EVP_PKEY_decrypt_init(ctx_.get()) == 1;
}

DataPointer EVPKeyCtxPointer::derive() const {
  if (!ctx_) return {};
  size_t len = 0;
  if (EVP_PKEY_derive(ctx_.get(), nullptr, &len) != 1) return {};
  auto data = DataPointer::Alloc(len);
  if (!data) return {};
  if (EVP_PKEY_derive(
          ctx_.get(), static_cast<unsigned char*>(data.get()), &len) != 1) {
    return {};
  }
  return data;
}

EVPKeyPointer EVPKeyCtxPointer::paramgen() const {
  if (!ctx_) return {};
  EVP_PKEY* key = nullptr;
  if (EVP_PKEY_paramgen(ctx_.get(), &key) != 1) return {};
  return EVPKeyPointer(key);
}

bool EVPKeyCtxPointer::publicCheck() const {
  if (!ctx_) return false;
#ifndef OPENSSL_IS_BORINGSSL
  return EVP_PKEY_public_check_quick(ctx_.get()) == 1;
#elif defined(OPENSSL_IS_BORINGSSL)
  // Boringssl appears not to support this operation.
  // TODO(jasnell): Is there an alternative approach that Boringssl does
  // support?
  return true;
#endif
}

bool EVPKeyCtxPointer::privateCheck() const {
  if (!ctx_) return false;
#ifndef OPENSSL_IS_BORINGSSL
  return EVP_PKEY_check(ctx_.get()) == 1;
#elif defined(OPENSSL_IS_BORINGSSL)
  // Boringssl appears not to support this operation.
  // TODO(jasnell): Is there an alternative approach that Boringssl does
  // support?
  return true;
#endif
}

bool EVPKeyCtxPointer::verify(const Buffer<const unsigned char>& sig,
                              const Buffer<const unsigned char>& data) {
  if (!ctx_) return false;
  return EVP_PKEY_verify(ctx_.get(), sig.data, sig.len, data.data, data.len) ==
         1;
}

DataPointer EVPKeyCtxPointer::sign(const Buffer<const unsigned char>& data) {
  if (!ctx_) return {};
  size_t len = 0;
  if (EVP_PKEY_sign(ctx_.get(), nullptr, &len, data.data, data.len) != 1) {
    return {};
  }
  auto buf = DataPointer::Alloc(len);
  if (!buf) return {};
  if (EVP_PKEY_sign(ctx_.get(),
                    static_cast<unsigned char*>(buf.get()),
                    &len,
                    data.data,
                    data.len) != 1) {
    return {};
  }
  return buf.resize(len);
}

bool EVPKeyCtxPointer::signInto(const Buffer<const unsigned char>& data,
                                Buffer<unsigned char>* sig) {
  if (!ctx_) return false;
  size_t len = sig->len;
  if (EVP_PKEY_sign(ctx_.get(), sig->data, &len, data.data, data.len) != 1) {
    return false;
  }
  sig->len = len;
  return true;
}

// ============================================================================

namespace {

using EVP_PKEY_cipher_init_t = int(EVP_PKEY_CTX* ctx);
using EVP_PKEY_cipher_t = int(EVP_PKEY_CTX* ctx,
                              unsigned char* out,
                              size_t* outlen,
                              const unsigned char* in,
                              size_t inlen);

template <EVP_PKEY_cipher_init_t init, EVP_PKEY_cipher_t cipher>
DataPointer RSA_Cipher(const EVPKeyPointer& key,
                       const Rsa::CipherParams& params,
                       const Buffer<const void> in) {
  if (!key) return {};
  EVPKeyCtxPointer ctx = key.newCtx();

  const Digest& mgf1_digest =
      params.mgf1_digest != nullptr ? params.mgf1_digest : params.digest;
  if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) ||
      (params.digest != nullptr &&
       (!ctx.setRsaOaepMd(params.digest) || !ctx.setRsaMgf1Md(mgf1_digest)))) {
    return {};
  }

  if (params.label.len != 0 && params.label.data != nullptr &&
      !ctx.setRsaOaepLabel(DataPointer::Copy(params.label))) {
    return {};
  }

  size_t out_len = 0;
  if (cipher(ctx.get(),
             nullptr,
             &out_len,
             reinterpret_cast<const unsigned char*>(in.data),
             in.len) <= 0) {
    return {};
  }

  auto buf = DataPointer::Alloc(out_len);
  if (!buf) return {};

  if (cipher(ctx.get(),
             static_cast<unsigned char*>(buf.get()),
             &out_len,
             static_cast<const unsigned char*>(in.data),
             in.len) <= 0) {
    return {};
  }

  return buf.resize(out_len);
}

template <EVP_PKEY_cipher_init_t init, EVP_PKEY_cipher_t cipher>
DataPointer CipherImpl(const EVPKeyPointer& key,
                       const Rsa::CipherParams& params,
                       const Buffer<const void> in) {
  if (!key) return {};
  EVPKeyCtxPointer ctx = key.newCtx();
  if (!ctx || init(ctx.get()) <= 0 || !ctx.setRsaPadding(params.padding) ||
      (params.digest != nullptr && !ctx.setRsaOaepMd(params.digest)) ||
      (params.mgf1_digest != nullptr &&
       !ctx.setRsaMgf1Md(params.mgf1_digest))) {
    return {};
  }

  if (params.label.len != 0 && params.label.data != nullptr &&
      !ctx.setRsaOaepLabel(DataPointer::Copy(params.label))) {
    return {};
  }

  size_t out_len = 0;
  if (cipher(ctx.get(),
             nullptr,
             &out_len,
             static_cast<const unsigned char*>(in.data),
             in.len) <= 0) {
    return {};
  }

  auto buf = DataPointer::Alloc(out_len);
  if (!buf) return {};

  if (cipher(ctx.get(),
             static_cast<unsigned char*>(buf.get()),
             &out_len,
             static_cast<const unsigned char*>(in.data),
             in.len) <= 0) {
    return {};
  }

  return buf.resize(out_len);
}
}  // namespace

Rsa::OtherPrimeInfoPointer::OtherPrimeInfoPointer(BignumPointer&& r,
                                                  BignumPointer&& d,
                                                  BignumPointer&& t)
    : r(r.release()), d(d.release()), t(t.release()) {}

#if NCRYPTO_USE_OPENSSL_PROVIDER
namespace {
// Normalizes a provider digest name such as "SHA2-256" to the long name the
// rest of the key details use ("sha256"). The returned storage has static
// lifetime, which the string_view fields of PssParams require.
const char* RsaPssDigestLongName(const char* name) {
  const EVP_MD* md = EVP_get_digestbyname(name);
  if (md == nullptr) return nullptr;
  const int nid = EVP_MD_get_type(md);
  return nid != NID_undef ? OBJ_nid2ln(nid) : nullptr;
}

bool ReadRsaPssParams(const EVP_PKEY* pkey, Rsa::PssParams* params) {
  // The RSASSA-PSS-params sequence is exposed as a unit. The salt length is
  // readable whenever the sequence is present, including when it is empty
  // because every field carried its default, and unreadable when the algorithm
  // identifier has no parameters at all. That is the distinction between a
  // restricted key and an unrestricted one.
  int salt_length = 0;
  // TODO(panva): In a semver-major, reject malformed RSA-PSS parameters
  // at key import instead of omitting asymmetricKeyDetails fields.
  if (EVP_PKEY_get_int_param(
          pkey, OSSL_PKEY_PARAM_RSA_PSS_SALTLEN, &salt_length) != 1 ||
      salt_length < 0) {
    return false;
  }
  params->salt_length = salt_length;

  // The provider may omit default SHA-1 digest parameters. Keep the initialized
  // defaults when a digest name is absent or cannot be resolved.
  char name[80];
  if (EVP_PKEY_get_utf8_string_param(
          pkey, OSSL_PKEY_PARAM_RSA_DIGEST, name, sizeof(name), nullptr) == 1) {
    if (const char* long_name = RsaPssDigestLongName(name)) {
      params->digest = long_name;
    }
  }

  if (EVP_PKEY_get_utf8_string_param(
          pkey, OSSL_PKEY_PARAM_RSA_MGF1_DIGEST, name, sizeof(name), nullptr) ==
      1) {
    if (const char* long_name = RsaPssDigestLongName(name)) {
      params->mgf1_digest = long_name;
    }
  }

  return true;
}

bool SetRsaPssHashAlgorithm(X509_ALGOR** out, const Digest& digest) {
  if (EVP_MD_is_a(digest.get(), "SHA1")) return true;

  X509AlgorPointer algorithm(X509_ALGOR_new());
  if (!algorithm) return false;
  X509_ALGOR_set_md(algorithm.get(), digest.get());
  *out = algorithm.release();
  return true;
}

bool SetRsaPssMaskGenAlgorithm(X509_ALGOR** out, const Digest& digest) {
  if (EVP_MD_is_a(digest.get(), "SHA1")) return true;

  X509AlgorPointer hash_algorithm(X509_ALGOR_new());
  if (!hash_algorithm) return false;
  X509_ALGOR_set_md(hash_algorithm.get(), digest.get());

  ASN1StringPointer hash_algorithm_der(ASN1_item_pack(
      hash_algorithm.get(), ASN1_ITEM_rptr(X509_ALGOR), nullptr));
  if (!hash_algorithm_der) return false;

  X509AlgorPointer algorithm(X509_ALGOR_new());
  if (!algorithm || X509_ALGOR_set0(algorithm.get(),
                                    OBJ_nid2obj(NID_mgf1),
                                    V_ASN1_SEQUENCE,
                                    hash_algorithm_der.get()) != 1) {
    return false;
  }
  hash_algorithm_der.release();
  *out = algorithm.release();
  return true;
}

ASN1StringPointer EncodeRsaPssParams(const Rsa::PssParams& params) {
  const Digest digest = Digest::FromName(params.digest.data());
  if (!digest) return {};

  const Digest mgf1_digest = params.mgf1_digest
                                 ? Digest::FromName(params.mgf1_digest->data())
                                 : digest;
  if (!mgf1_digest) return {};

  RsaPssParamsPointer pss(RSA_PSS_PARAMS_new());
  if (!pss || !SetRsaPssHashAlgorithm(&pss->hashAlgorithm, digest) ||
      !SetRsaPssMaskGenAlgorithm(&pss->maskGenAlgorithm, mgf1_digest)) {
    return {};
  }

  if (params.salt_length != 20) {
    pss->saltLength = ASN1_INTEGER_new();
    if (pss->saltLength == nullptr ||
        ASN1_INTEGER_set_int64(pss->saltLength, params.salt_length) != 1) {
      return {};
    }
  }

  return ASN1StringPointer(
      ASN1_item_pack(pss.get(), ASN1_ITEM_rptr(RSA_PSS_PARAMS), nullptr));
}
}  // namespace

Rsa::Rsa() : rsa_(false) {}

Rsa::Rsa(const EVP_PKEY* pkey, Selection selection) : Rsa() {
  rsa_pss_ = EVPKeyPointer::isA(pkey, KeyAlgorithm::RSA_PSS);
  if (!EVPKeyPointer::isA(pkey, KeyAlgorithm::RSA) && !rsa_pss_) return;
  if (!GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_N, &n_) ||
      !GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_E, &e_)) {
    return;
  }
  if (rsa_pss_) {
    MarkPopErrorOnReturn pop_errors;
    PssParams params;
    if (ReadRsaPssParams(pkey, &params)) pss_params_ = params;
  }
  if (selection == Selection::Public) {
    rsa_ = true;
    return;
  }
  if (!GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_D, &d_) ||
      !GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_FACTOR1, &p_) ||
      !GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_FACTOR2, &q_) ||
      !GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_EXPONENT1, &dp_) ||
      !GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_EXPONENT2, &dq_) ||
      !GetOptionalPKeyBnParam(pkey, OSSL_PKEY_PARAM_RSA_COEFFICIENT1, &qi_)) {
    return;
  }

  for (const auto& names : kRsaOtherPrimeParamNames) {
    OtherPrimeInfoPointer info;
    if (!GetOptionalPKeyBnParam(pkey, names.factor, &info.r) ||
        !GetOptionalPKeyBnParam(pkey, names.exponent, &info.d) ||
        !GetOptionalPKeyBnParam(pkey, names.coefficient, &info.t)) {
      return;
    }

    if (!info.r && !info.d && !info.t) break;
    if (!info.r || !info.d || !info.t) return;
    other_prime_infos_.push_back(std::move(info));
  }

  rsa_ = true;
}
#elif NCRYPTO_USE_BORINGSSL
Rsa::Rsa() : rsa_(nullptr) {}
Rsa::Rsa(OSSL3_CONST RSA* ptr) : rsa_(ptr) {}
#else
#error "Unsupported crypto backend"
#endif

Rsa Rsa::PublicOnly(const EVPKeyPointer& key) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return Rsa(key.get(), Selection::Public);
#elif NCRYPTO_USE_BORINGSSL
  return key;
#else
#error "Unsupported crypto backend"
#endif
}

const Rsa::PublicKey Rsa::getPublicKey() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!rsa_) return {};
  return PublicKey{n_.get(), e_.get(), d_.get()};
#elif NCRYPTO_USE_BORINGSSL
  if (rsa_ == nullptr) return {};
  PublicKey key;
  RSA_get0_key(rsa_, &key.n, &key.e, &key.d);
  return key;
#else
#error "Unsupported crypto backend"
#endif
}

const Rsa::PrivateKey Rsa::getPrivateKey() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!rsa_) return {};
  return PrivateKey{p_.get(), q_.get(), dp_.get(), dq_.get(), qi_.get()};
#elif NCRYPTO_USE_BORINGSSL
  if (rsa_ == nullptr) return {};
  PrivateKey key;
  RSA_get0_factors(rsa_, &key.p, &key.q);
  RSA_get0_crt_params(rsa_, &key.dp, &key.dq, &key.qi);
  return key;
#else
#error "Unsupported crypto backend"
#endif
}

const Rsa::OtherPrimeInfos Rsa::getOtherPrimeInfos() const {
  OtherPrimeInfos infos;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  infos.reserve(other_prime_infos_.size());
  for (const auto& info : other_prime_infos_) {
    infos.push_back({info.r.get(), info.d.get(), info.t.get()});
  }
#endif
  return infos;
}

bool Rsa::checkPrimeProduct() const {
  const auto pub = getPublicKey();
  const auto priv = getPrivateKey();
  if (pub.n == nullptr || priv.p == nullptr || priv.q == nullptr) return false;
  auto product = BignumPointer::New();
  BignumCtxPointer ctx(BN_CTX_new());
  if (!product || !ctx ||
      BN_mul(product.get(), priv.p, priv.q, ctx.get()) != 1) {
    return false;
  }
  for (const auto& info : getOtherPrimeInfos()) {
    auto next = BignumPointer::New();
    if (!next || BN_mul(next.get(), product.get(), info.r, ctx.get()) != 1) {
      return false;
    }
    product = std::move(next);
  }
  return BN_cmp(product.get(), pub.n) == 0;
}

const std::optional<Rsa::PssParams> Rsa::getPssParams() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return pss_params_;
#elif NCRYPTO_USE_BORINGSSL
  if (rsa_ == nullptr) return std::nullopt;
  const RSA_PSS_PARAMS* params = RSA_get0_pss_params(rsa_);
  if (params == nullptr) return std::nullopt;
  Rsa::PssParams ret{
      .digest = OBJ_nid2ln(NID_sha1),
      .mgf1_digest = OBJ_nid2ln(NID_sha1),
      .salt_length = 20,
  };

  if (params->hashAlgorithm != nullptr) {
    const ASN1_OBJECT* hash_obj;
    X509_ALGOR_get0(&hash_obj, nullptr, nullptr, params->hashAlgorithm);
    ret.digest = OBJ_nid2ln(OBJ_obj2nid(hash_obj));
  }

  if (params->maskGenAlgorithm != nullptr) {
    const ASN1_OBJECT* mgf_obj;
    X509_ALGOR_get0(&mgf_obj, nullptr, nullptr, params->maskGenAlgorithm);
    int mgf_nid = OBJ_obj2nid(mgf_obj);
    if (mgf_nid == NID_mgf1) {
      const ASN1_OBJECT* mgf1_hash_obj;
      X509_ALGOR_get0(&mgf1_hash_obj, nullptr, nullptr, params->maskHash);
      ret.mgf1_digest = OBJ_nid2ln(OBJ_obj2nid(mgf1_hash_obj));
    }
  }

  if (params->saltLength != nullptr) {
    if (ASN1_INTEGER_get_int64(&ret.salt_length, params->saltLength) != 1) {
      return std::nullopt;
    }
  }
  return ret;
#else
#error "Unsupported crypto backend"
#endif
}

BIOPointer Rsa::derPublicKey() const {
  auto bio = BIOPointer::NewMem();
  if (!bio) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  auto pkey = EVPKeyPointer::NewRSA(*this);
  if (!pkey) return {};
  if (!rsa_pss_) {
    if (i2d_PUBKEY_bio(bio.get(), pkey.get()) != 1) return {};
    return bio;
  }

  X509_PUBKEY* raw_pubkey = nullptr;
  const int result = X509_PUBKEY_set(&raw_pubkey, pkey.get());
  X509PubkeyPointer pubkey(raw_pubkey);
  if (result != 1) return {};

  int parameter_type = V_ASN1_UNDEF;
  ASN1StringPointer parameters;
  if (pss_params_) {
    parameters = EncodeRsaPssParams(*pss_params_);
    if (!parameters) return {};
    parameter_type = V_ASN1_SEQUENCE;
  }

  if (X509_PUBKEY_set0_param(pubkey.get(),
                             OBJ_nid2obj(NID_rsaEncryption),
                             parameter_type,
                             parameters.get(),
                             nullptr,
                             0) != 1) {
    return {};
  }
  parameters.release();
  if (i2d_X509_PUBKEY_bio(bio.get(), pubkey.get()) != 1) return {};
#elif NCRYPTO_USE_BORINGSSL
  if (rsa_ == nullptr || i2d_RSA_PUBKEY_bio(bio.get(), rsa_) != 1) return {};
#else
#error "Unsupported crypto backend"
#endif
  return bio;
}

bool Rsa::setPublicKey(BignumPointer&& n, BignumPointer&& e) {
  if (!n || !e) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  n_.reset(n.release());
  e_.reset(e.release());
  rsa_ = true;
  return true;
#elif NCRYPTO_USE_BORINGSSL
  if (RSA_set0_key(const_cast<RSA*>(rsa_), n.get(), e.get(), nullptr) == 1) {
    n.release();
    e.release();
    return true;
  }
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

bool Rsa::setPrivateKey(BignumPointer&& d,
                        BignumPointer&& q,
                        BignumPointer&& p,
                        BignumPointer&& dp,
                        BignumPointer&& dq,
                        BignumPointer&& qi,
                        OtherPrimeInfoPointers&& other_prime_infos) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!d || !q || !p || !dp || !dq || !qi) return false;
  for (const auto& info : other_prime_infos) {
    if (!info.r || !info.d || !info.t) return false;
  }
  d_.reset(d.release());
  q_.reset(q.release());
  p_.reset(p.release());
  dp_.reset(dp.release());
  dq_.reset(dq.release());
  qi_.reset(qi.release());
  other_prime_infos_ = std::move(other_prime_infos);
  rsa_ = n_ != nullptr && e_ != nullptr;
  return rsa_;
#elif NCRYPTO_USE_BORINGSSL
  if (!RSA_set0_key(const_cast<RSA*>(rsa_), nullptr, nullptr, d.get())) {
    return false;
  }
  d.release();

  if (!RSA_set0_factors(const_cast<RSA*>(rsa_), p.get(), q.get())) {
    return false;
  }
  p.release();
  q.release();

  if (!RSA_set0_crt_params(
          const_cast<RSA*>(rsa_), dp.get(), dq.get(), qi.get())) {
    return false;
  }
  dp.release();
  dq.release();
  qi.release();

  if (!other_prime_infos.empty()) return false;
  return true;
#else
#error "Unsupported crypto backend"
#endif
}

DataPointer Rsa::encrypt(const EVPKeyPointer& key,
                         const Rsa::CipherParams& params,
                         const Buffer<const void> in) {
  if (!key) return {};
  return RSA_Cipher<EVP_PKEY_encrypt_init, EVP_PKEY_encrypt>(key, params, in);
}

DataPointer Rsa::decrypt(const EVPKeyPointer& key,
                         const Rsa::CipherParams& params,
                         const Buffer<const void> in) {
  if (!key) return {};
  return RSA_Cipher<EVP_PKEY_decrypt_init, EVP_PKEY_decrypt>(key, params, in);
}

DataPointer Cipher::encrypt(const EVPKeyPointer& key,
                            const CipherParams& params,
                            const Buffer<const void> in) {
  // public operation
  return CipherImpl<EVP_PKEY_encrypt_init, EVP_PKEY_encrypt>(key, params, in);
}

DataPointer Cipher::decrypt(const EVPKeyPointer& key,
                            const CipherParams& params,
                            const Buffer<const void> in) {
  // private operation
  return CipherImpl<EVP_PKEY_decrypt_init, EVP_PKEY_decrypt>(key, params, in);
}

DataPointer Cipher::sign(const EVPKeyPointer& key,
                         const CipherParams& params,
                         const Buffer<const void> in) {
  // private operation
  return CipherImpl<EVP_PKEY_sign_init, EVP_PKEY_sign>(key, params, in);
}

DataPointer Cipher::recover(const EVPKeyPointer& key,
                            const CipherParams& params,
                            const Buffer<const void> in) {
  // public operation
  return CipherImpl<EVP_PKEY_verify_recover_init, EVP_PKEY_verify_recover>(
      key, params, in);
}

namespace {
struct CipherCallbackContext {
  Cipher::CipherNameCallback cb;
  void operator()(const char* name) { cb(name); }
};

#if NCRYPTO_USE_OPENSSL_PROVIDER
template <class TypeName,
          TypeName* fetch_type(OSSL_LIB_CTX*, const char*, const char*),
          void free_type(TypeName*),
          const TypeName* getbyname(const char*),
          const char* getname(const TypeName*)>
void array_push_back(const TypeName* evp_ref,
                     const char* from,
                     const char* to,
                     void* arg) {
  if (from == nullptr) return;

  const TypeName* real_instance = getbyname(from);
  if (!real_instance) return;

  const char* real_name = getname(real_instance);
  if (!real_name) return;

  // EVP_*_fetch() does not support alias names, so we need to pass it the
  // real/original algorithm name.
  // We use EVP_*_fetch() as a filter here because it will only return an
  // instance if the algorithm is supported by the public OpenSSL APIs (some
  // algorithms are used internally by OpenSSL and are also passed to this
  // callback).
  DeleteFnPtr<TypeName, free_type> fetched(
      fetch_type(nullptr, real_name, nullptr));
  if (!IsSupportedFetchedCipher(fetched.get())) return;

  auto& cb = *(static_cast<CipherCallbackContext*>(arg));
  cb(from);
}

void array_push_back_provider_name(const char* name, void* arg) {
  if (name == nullptr) return;

  const std::string_view name_view(name);
  const bool is_dotted_decimal =
      name_view.find('.') != std::string_view::npos &&
      std::all_of(name_view.begin(), name_view.end(), [](unsigned char c) {
        return (c >= '0' && c <= '9') || c == '.';
      });
  if (is_dotted_decimal) return;

  std::string normalized_name(name_view);
  std::transform(normalized_name.begin(),
                 normalized_name.end(),
                 normalized_name.begin(),
                 [](unsigned char c) {
                   if (c >= 'A' && c <= 'Z') {
                     return static_cast<char>(c + ('a' - 'A'));
                   }
                   return static_cast<char>(c);
                 });
  auto& cb = *(static_cast<CipherCallbackContext*>(arg));
  cb(normalized_name.c_str());
}

void array_push_back_provider(EVP_CIPHER* cipher, void* arg) {
  const char* name = EVP_CIPHER_get0_name(cipher);
  if (name == nullptr) return;

  DeleteFnPtr<EVP_CIPHER, EVP_CIPHER_free> fetched(
      EVP_CIPHER_fetch(nullptr, name, nullptr));
  if (!IsSupportedFetchedCipher(fetched.get())) return;

  EVP_CIPHER_names_do_all(fetched.get(), array_push_back_provider_name, arg);
}
#elif NCRYPTO_USE_BORINGSSL
template <class TypeName>
void array_push_back(const TypeName* evp_ref,
                     const char* from,
                     const char* to,
                     void* arg) {
  if (!from) return;
  auto& cb = *(static_cast<CipherCallbackContext*>(arg));
  cb(from);
}
#else
#error "Unsupported crypto backend"
#endif
}  // namespace

void Cipher::ForEach(Cipher::CipherNameCallback callback) {
  ClearErrorOnReturn clearErrorOnReturn;
  CipherCallbackContext context;
  context.cb = std::move(callback);

#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
  for (const auto& cipher : kBoringSSLCiphers) {
    static_cast<void>(cipher.get);
    context.cb(cipher.name);
  }
#else
  EVP_CIPHER_do_all_sorted(
#if NCRYPTO_USE_OPENSSL_PROVIDER
      array_push_back<EVP_CIPHER,
                      EVP_CIPHER_fetch,
                      EVP_CIPHER_free,
                      EVP_get_cipherbyname,
                      EVP_CIPHER_get0_name>,
#elif NCRYPTO_USE_BORINGSSL
      array_push_back<EVP_CIPHER>,
#else
#error "Unsupported crypto backend"
#endif
      &context);
#if NCRYPTO_USE_OPENSSL_PROVIDER
  EVP_CIPHER_do_all_provided(nullptr, array_push_back_provider, &context);
#endif
#endif
}

// ============================================================================

#if NCRYPTO_USE_OPENSSL_PROVIDER
Ec::Ec() : ec_(nullptr), pub_(nullptr) {}

Ec::Ec(const EVP_PKEY* pkey) : Ec() {
  if (!EVPKeyPointer::isA(pkey, KeyAlgorithm::EC)) return;
  char group_name[80];
  size_t group_name_len = 0;
  if (EVP_PKEY_get_utf8_string_param(pkey,
                                     OSSL_PKEY_PARAM_GROUP_NAME,
                                     group_name,
                                     sizeof(group_name),
                                     &group_name_len) != 1) {
    return;
  }

  const int nid = GetCurveIdFromName(group_name);
  if (nid == NID_undef) return;
  ec_.reset(EC_GROUP_new_by_curve_name(nid));
  if (!ec_) return;

  size_t public_key_len = 0;
  if (EVP_PKEY_get_octet_string_param(
          pkey, OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &public_key_len) != 1) {
    return;
  }

  auto public_key = DataPointer::Alloc(public_key_len);
  if (!public_key ||
      EVP_PKEY_get_octet_string_param(
          pkey,
          OSSL_PKEY_PARAM_PUB_KEY,
          static_cast<unsigned char*>(public_key.get()),
          public_key.size(),
          &public_key_len) != 1 ||
      public_key_len == 0) {
    ec_.reset();
    return;
  }

  const auto* public_key_data =
      static_cast<const unsigned char*>(public_key.get());
  switch (public_key_data[0]) {
    case POINT_CONVERSION_COMPRESSED:
    case POINT_CONVERSION_COMPRESSED + 1:
      form_ = POINT_CONVERSION_COMPRESSED;
      break;
    case POINT_CONVERSION_UNCOMPRESSED:
      form_ = POINT_CONVERSION_UNCOMPRESSED;
      break;
    case POINT_CONVERSION_HYBRID:
    case POINT_CONVERSION_HYBRID + 1:
      form_ = POINT_CONVERSION_HYBRID;
      break;
    default:
      ec_.reset();
      return;
  }

  auto point = ECPointPointer::New(ec_.get());
  if (!point ||
      !point.setFromBuffer({public_key_data, public_key_len}, ec_.get())) {
    ec_.reset();
    return;
  }
  pub_.reset(point.release());
}
#elif NCRYPTO_USE_BORINGSSL
Ec::Ec() : ec_(nullptr) {}
Ec::Ec(OSSL3_CONST EC_KEY* key) : ec_(key) {}
#else
#error "Unsupported crypto backend"
#endif

const EC_GROUP* Ec::getGroup() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return ec_.get();
#elif NCRYPTO_USE_BORINGSSL
  return ECKeyPointer::GetGroup(ec_);
#else
#error "Unsupported crypto backend"
#endif
}

const EC_POINT* Ec::getPublicKey() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return pub_.get();
#elif NCRYPTO_USE_BORINGSSL
  return ECKeyPointer::GetPublicKey(ec_);
#else
#error "Unsupported crypto backend"
#endif
}

point_conversion_form_t Ec::getPointConversionForm() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  return form_;
#elif NCRYPTO_USE_BORINGSSL
  return EC_KEY_get_conv_form(ec_);
#else
#error "Unsupported crypto backend"
#endif
}

int Ec::getCurve() const {
  return EC_GROUP_get_curve_name(getGroup());
}

BIOPointer Ec::ExportPrivatePkcs8(const EVPKeyPointer& key) {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (!key || !key.isA(KeyAlgorithm::EC)) return {};
  auto ec = ECKeyPointer(key).clone();
  if (!ec) return {};
#if NCRYPTO_USE_LEGACY_KEY_TYPES
  // Decoding an ECPrivateKey without publicKey reconstructs the public point
  // but retains a flag that omits it from subsequent encodings.
  EC_KEY_set_enc_flags(ec.get(),
                       EC_KEY_get_enc_flags(ec.get()) & ~EC_PKEY_NO_PUBKEY);
#endif
  auto export_key = EVPKeyPointer::New();
  if (!export_key || !export_key.set(ec)) return {};
  auto encoded = export_key.writePrivateKey({});
  if (!encoded) return {};
  return std::move(encoded.value);
}

DataPointer Ec::TryExportPublic(const EVPKeyPointer& key,
                                point_conversion_form_t form) {
  if (!key || form != POINT_CONVERSION_UNCOMPRESSED) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  {
    MarkPopErrorOnReturn pop_errors;
    size_t length = 0;
    if (EVP_PKEY_get_octet_string_param(
            key.get(), OSSL_PKEY_PARAM_PUB_KEY, nullptr, 0, &length) == 1) {
      auto bytes = DataPointer::Alloc(length);
      if (bytes && length != 0 &&
          EVP_PKEY_get_octet_string_param(key.get(),
                                          OSSL_PKEY_PARAM_PUB_KEY,
                                          bytes.get<unsigned char>(),
                                          length,
                                          &length) == 1 &&
          (bytes.get<unsigned char>()[0] & ~1) == form) {
        return bytes.resize(length);
      }
    }
  }
#endif
  return {};
}

DataPointer Ec::ExportPrivate(const EVPKeyPointer& key) {
  if (!key) return {};
#if NCRYPTO_USE_OPENSSL_PROVIDER
  {
    MarkPopErrorOnReturn pop_errors;
    BignumPointer priv;
    BignumPointer order;
    if (GetPKeyBnParam(key.get(), OSSL_PKEY_PARAM_PRIV_KEY, &priv) &&
        GetPKeyBnParam(key.get(), OSSL_PKEY_PARAM_EC_ORDER, &order)) {
      return priv.encodePadded(order.byteLength());
    }
  }
#endif
  ECKeyPointer ec(key);
  if (!ec || ec.getPrivateKey() == nullptr) return {};
  auto order = BignumPointer::New();
  if (!order || !EC_GROUP_get_order(ec.getGroup(), order.get(), nullptr))
    return {};
  return BignumPointer::EncodePadded(ec.getPrivateKey(), order.byteLength());
}

bool Ec::GetKeyComponents(const EVPKeyPointer& key,
                          BignumPointer* x,
                          BignumPointer* y,
                          BignumPointer* priv,
                          int* degree) {
  if (!key) return false;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  const int nid = GetCurveId(key);
  switch (nid) {
    case NID_X9_62_prime256v1:
    case NID_secp256k1:
      *degree = 256;
      break;
    case NID_secp384r1:
      *degree = 384;
      break;
    case NID_secp521r1:
      *degree = 521;
      break;
    default:
      *degree = 0;
  }
  if (*degree != 0) {
    MarkPopErrorOnReturn pop_errors;
    unsigned char x_bytes[66]{};
    unsigned char y_bytes[66]{};
    const size_t width = (*degree + 7) / 8;
    OSSL_PARAM params[] = {
        OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_EC_PUB_X, x_bytes, width),
        OSSL_PARAM_construct_BN(OSSL_PKEY_PARAM_EC_PUB_Y, y_bytes, width),
        OSSL_PARAM_construct_end(),
    };
    if (EVP_PKEY_get_params(key.get(), params) == 1 &&
        OSSL_PARAM_modified(&params[0]) && OSSL_PARAM_modified(&params[1])) {
      x->reset(BN_native2bn(x_bytes, width, nullptr));
      y->reset(BN_native2bn(y_bytes, width, nullptr));
      return *x && *y &&
             (priv == nullptr ||
              GetPKeyBnParam(key.get(), OSSL_PKEY_PARAM_PRIV_KEY, priv));
    }
  }
#endif
  ECKeyPointer ec(key);
  if (!ec || ec.getPublicKey() == nullptr) return false;
  *degree = EC_GROUP_get_degree(ec.getGroup());
  x->reset(BN_new());
  y->reset(BN_new());
  if (!*x || !*y ||
      EC_POINT_get_affine_coordinates(
          ec.getGroup(), ec.getPublicKey(), x->get(), y->get(), nullptr) != 1) {
    return false;
  }
  if (priv != nullptr) {
    if (ec.getPrivateKey() == nullptr) return false;
    priv->reset(BN_dup(ec.getPrivateKey()));
    if (!*priv) return false;
  }
  return true;
}

int Ec::GetCurveId(const EVPKeyPointer& key) {
  if (!key) return NID_undef;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  char name[80];
  size_t length = 0;
  if (EVP_PKEY_get_utf8_string_param(
          key.get(), OSSL_PKEY_PARAM_GROUP_NAME, name, sizeof(name), &length) !=
      1) {
    return NID_undef;
  }
  return GetCurveIdFromName(name);
#elif NCRYPTO_USE_BORINGSSL
  const EC_KEY* ec = key;
  if (ec == nullptr) return NID_undef;
  const EC_GROUP* group = EC_KEY_get0_group(ec);
  return group == nullptr ? NID_undef : EC_GROUP_get_curve_name(group);
#else
#error "Unsupported crypto backend"
#endif
}

std::optional<std::string> Ec::GetCurveName(const EVPKeyPointer& key) {
  if (!key) return std::nullopt;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  size_t length = 0;
  if (EVP_PKEY_get_utf8_string_param(
          key.get(), OSSL_PKEY_PARAM_GROUP_NAME, nullptr, 0, &length) != 1) {
    return std::nullopt;
  }
  std::string name(length, '\0');
  if (EVP_PKEY_get_utf8_string_param(key.get(),
                                     OSSL_PKEY_PARAM_GROUP_NAME,
                                     name.data(),
                                     name.size() + 1,
                                     &length) != 1) {
    return std::nullopt;
  }
  name.resize(length);
  // Preserve the public short names for the curves OpenSSL already knows.
  const int nid = GetCurveIdFromName(name.c_str());
  return nid == NID_undef ? name : std::string(OBJ_nid2sn(nid));
#elif NCRYPTO_USE_BORINGSSL
  const int nid = GetCurveId(key);
  if (nid == NID_undef) return std::nullopt;
  return std::string(OBJ_nid2sn(nid));
#else
#error "Unsupported crypto backend"
#endif
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
namespace {
bool IsAvailableEcGroup(const char* name) {
  MarkPopErrorOnReturn mark;
  auto ctx = EVPKeyCtxPointer::NewFromAlgorithm(KeyAlgorithm::EC);
  return ctx.initForParamgen() &&
         ctx.setEcParameters(name, OPENSSL_EC_NAMED_CURVE) && ctx.paramgen();
}
}  // namespace
#endif

bool Ec::CheckCurveName(const char* name) {
  if (name == nullptr) return false;
  if (GetCurveIdFromName(name) != NID_undef) return true;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  // Keep invalid names a synchronous argument error. Generation contexts can
  // defer rejecting a group until parameter generation. Use the same parameter
  // generation path as key generation without requiring parameter import.
  return IsAvailableEcGroup(name);
#elif NCRYPTO_USE_BORINGSSL
  return false;
#else
#error "Unsupported crypto backend"
#endif
}

int Ec::GetCurveIdFromName(const char* name) {
  int nid = EC_curve_nist2nid(name);
  if (nid == NID_undef) {
    nid = OBJ_sn2nid(name);
  }
  return nid;
}

const KeyAlgorithm* Ec::GetNamedKeyAlgorithm(const char* name) {
  // Preserve the aliases accepted by the historical namedCurve option.
  const int nid = GetCurveIdFromName(name);
  if (nid == NID_undef) return nullptr;
  const auto* algorithm = KeyAlgorithm::FromName(OBJ_nid2sn(nid));
  return algorithm != nullptr && algorithm->isOkp() ? algorithm : nullptr;
}

bool Ec::GetCurves(Ec::GetCurveCallback callback) {
  const size_t count = EC_get_builtin_curves(nullptr, 0);
  std::vector<EC_builtin_curve> curves(count);
  if (EC_get_builtin_curves(curves.data(), count) != count) {
    return false;
  }
  for (const auto& curve : curves) {
    const char* name = OBJ_nid2sn(curve.nid);
#if NCRYPTO_USE_OPENSSL_PROVIDER
    if (!IsAvailableEcGroup(name)) continue;
#endif
    if (!callback(name)) return false;
  }
  return true;
}

// ============================================================================

EVPMDCtxPointer::EVPMDCtxPointer() : ctx_(nullptr) {}

EVPMDCtxPointer::EVPMDCtxPointer(EVP_MD_CTX* ctx) : ctx_(ctx) {}

EVPMDCtxPointer::EVPMDCtxPointer(EVPMDCtxPointer&& other) noexcept
    : ctx_(other.release()) {}

EVPMDCtxPointer& EVPMDCtxPointer::operator=(EVPMDCtxPointer&& other) noexcept {
  ctx_.reset(other.release());
  return *this;
}

EVPMDCtxPointer::~EVPMDCtxPointer() {
  reset();
}

void EVPMDCtxPointer::reset(EVP_MD_CTX* ctx) {
  ctx_.reset(ctx);
}

EVP_MD_CTX* EVPMDCtxPointer::release() {
  return ctx_.release();
}

bool EVPMDCtxPointer::digestInit(const EVP_MD* digest) {
  if (!ctx_) return false;
  return EVP_DigestInit_ex(ctx_.get(), digest, nullptr) > 0;
}

#if !defined(OPENSSL_IS_BORINGSSL) && OPENSSL_VERSION_PREREQ(4, 0)
bool EVPMDCtxPointer::digestInit(const EVP_MD* digest,
                                 const OSSL_PARAM* params) {
  if (!ctx_) return false;
  return EVP_DigestInit_ex2(ctx_.get(), digest, params) > 0;
}
#endif

bool EVPMDCtxPointer::digestUpdate(const Buffer<const void>& in) {
  if (!ctx_) return false;
  return EVP_DigestUpdate(ctx_.get(), in.data, in.len) > 0;
}

DataPointer EVPMDCtxPointer::digestFinal(size_t length) {
  if (!ctx_) return {};

  auto buf = DataPointer::Alloc(length);
  if (!buf) return {};

  Buffer<void> buffer = buf;

  if (!digestFinalInto(&buffer)) [[unlikely]] {
    return {};
  }

  return buf;
}

bool EVPMDCtxPointer::digestFinalInto(Buffer<void>* buf) {
  if (!ctx_) return false;

  auto ptr = static_cast<unsigned char*>(buf->data);

  int ret = (buf->len == getExpectedSize())
                ? EVP_DigestFinal_ex(ctx_.get(), ptr, nullptr)
                : EVP_DigestFinalXOF(ctx_.get(), ptr, buf->len);

  if (ret != 1) [[unlikely]]
    return false;

  return true;
}

size_t EVPMDCtxPointer::getExpectedSize() {
  if (!ctx_) return 0;
  return EVP_MD_CTX_size(ctx_.get());
}

size_t EVPMDCtxPointer::getDigestSize() const {
  return EVP_MD_size(getDigest());
}

const EVP_MD* EVPMDCtxPointer::getDigest() const {
  if (!ctx_) return nullptr;
  return GetDigestCtxMd(ctx_.get());
}

bool EVPMDCtxPointer::hasXofFlag() const {
  if (!ctx_) return false;
  return (EVP_MD_flags(getDigest()) & EVP_MD_FLAG_XOF) == EVP_MD_FLAG_XOF;
}

bool EVPMDCtxPointer::copyTo(const EVPMDCtxPointer& other) const {
  if (!ctx_ || !other) return {};
  if (EVP_MD_CTX_copy(other.get(), ctx_.get()) != 1) return false;
  return true;
}

std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::signInit(const EVPKeyPointer& key,
                                                       const Digest& digest) {
  EVP_PKEY_CTX* ctx = nullptr;
  if (!EVP_DigestSignInit(ctx_.get(), &ctx, digest, nullptr, key.get())) {
    return std::nullopt;
  }
  return ctx;
}

std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::verifyInit(
    const EVPKeyPointer& key, const Digest& digest) {
  EVP_PKEY_CTX* ctx = nullptr;
  if (!EVP_DigestVerifyInit(ctx_.get(), &ctx, digest, nullptr, key.get())) {
    return std::nullopt;
  }
  return ctx;
}

std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::signInitWithContext(
    const EVPKeyPointer& key,
    const Digest& digest,
    const Buffer<const unsigned char>& context_string) {
#ifdef OPENSSL_IS_BORINGSSL
  EVP_PKEY_CTX* ctx = nullptr;
  if (!EVP_DigestSignInit(ctx_.get(), &ctx, digest, nullptr, key.get())) {
    return std::nullopt;
  }
  if (EVP_PKEY_CTX_set1_signature_context_string(
          ctx, context_string.data, context_string.len) <= 0) {
    return std::nullopt;
  }
  return ctx;
#else
  EVP_PKEY_CTX* ctx = nullptr;

  // Ed25519 requires the INSTANCE param to switch into Ed25519ctx mode.
  // Without it, OpenSSL silently ignores the context string.
  if (key.isA(KeyAlgorithm::ED25519)) {
    const OSSL_PARAM params[] = {
        OSSL_PARAM_construct_utf8_string(
            kSignatureInstance, const_cast<char*>("Ed25519ctx"), 0),
        OSSL_PARAM_construct_octet_string(
            kSignatureContextString,
            const_cast<unsigned char*>(context_string.data),
            context_string.len),
        OSSL_PARAM_END};

    if (!EVP_DigestSignInit_ex(
            ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) {
      return std::nullopt;
    }
    return ctx;
  }

  const OSSL_PARAM params[] = {
      OSSL_PARAM_construct_octet_string(
          kSignatureContextString,
          const_cast<unsigned char*>(context_string.data),
          context_string.len),
      OSSL_PARAM_END};

  if (!EVP_DigestSignInit_ex(
          ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) {
    return std::nullopt;
  }
  return ctx;
#endif
}

std::optional<EVP_PKEY_CTX*> EVPMDCtxPointer::verifyInitWithContext(
    const EVPKeyPointer& key,
    const Digest& digest,
    const Buffer<const unsigned char>& context_string) {
#ifdef OPENSSL_IS_BORINGSSL
  EVP_PKEY_CTX* ctx = nullptr;
  if (!EVP_DigestVerifyInit(ctx_.get(), &ctx, digest, nullptr, key.get())) {
    return std::nullopt;
  }
  if (EVP_PKEY_CTX_set1_signature_context_string(
          ctx, context_string.data, context_string.len) <= 0) {
    return std::nullopt;
  }
  return ctx;
#else
  EVP_PKEY_CTX* ctx = nullptr;

  // Ed25519 requires the INSTANCE param to switch into Ed25519ctx mode.
  // Without it, OpenSSL silently ignores the context string.
  if (key.isA(KeyAlgorithm::ED25519)) {
    const OSSL_PARAM params[] = {
        OSSL_PARAM_construct_utf8_string(
            kSignatureInstance, const_cast<char*>("Ed25519ctx"), 0),
        OSSL_PARAM_construct_octet_string(
            kSignatureContextString,
            const_cast<unsigned char*>(context_string.data),
            context_string.len),
        OSSL_PARAM_END};

    if (!EVP_DigestVerifyInit_ex(
            ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) {
      return std::nullopt;
    }
    return ctx;
  }

  const OSSL_PARAM params[] = {
      OSSL_PARAM_construct_octet_string(
          kSignatureContextString,
          const_cast<unsigned char*>(context_string.data),
          context_string.len),
      OSSL_PARAM_END};

  if (!EVP_DigestVerifyInit_ex(
          ctx_.get(), &ctx, nullptr, nullptr, nullptr, key.get(), params)) {
    return std::nullopt;
  }
  return ctx;
#endif
}

DataPointer EVPMDCtxPointer::signOneShot(
    const Buffer<const unsigned char>& buf) const {
  if (!ctx_) return {};
  size_t len;
  if (!EVP_DigestSign(ctx_.get(), nullptr, &len, buf.data, buf.len)) {
    return {};
  }
  auto data = DataPointer::Alloc(len);
  if (!data) [[unlikely]]
    return {};

  if (!EVP_DigestSign(ctx_.get(),
                      static_cast<unsigned char*>(data.get()),
                      &len,
                      buf.data,
                      buf.len)) {
    return {};
  }
  return data;
}

DataPointer EVPMDCtxPointer::sign(
    const Buffer<const unsigned char>& buf) const {
  if (!ctx_) [[unlikely]]
    return {};
  size_t len;
  if (!EVP_DigestSignUpdate(ctx_.get(), buf.data, buf.len) ||
      !EVP_DigestSignFinal(ctx_.get(), nullptr, &len)) {
    return {};
  }
  auto data = DataPointer::Alloc(len);
  if (!data) [[unlikely]]
    return {};
  if (!EVP_DigestSignFinal(
          ctx_.get(), static_cast<unsigned char*>(data.get()), &len)) {
    return {};
  }
  return data.resize(len);
}

bool EVPMDCtxPointer::verify(const Buffer<const unsigned char>& buf,
                             const Buffer<const unsigned char>& sig) const {
  return verifyOneShot(buf, sig) == 1;
}

int EVPMDCtxPointer::verifyOneShot(
    const Buffer<const unsigned char>& buf,
    const Buffer<const unsigned char>& sig) const {
  if (!ctx_) return -1;
  return EVP_DigestVerify(ctx_.get(), sig.data, sig.len, buf.data, buf.len);
}

EVPMDCtxPointer EVPMDCtxPointer::New() {
  return EVPMDCtxPointer(EVP_MD_CTX_new());
}

// ============================================================================

bool extractP1363(const Buffer<const unsigned char>& buf,
                  unsigned char* dest,
                  size_t n) {
  auto asn1_sig = ECDSASigPointer::Parse(buf);
  if (!asn1_sig) return false;

  return BignumPointer::EncodePaddedInto(asn1_sig.r(), dest, n) > 0 &&
         BignumPointer::EncodePaddedInto(asn1_sig.s(), dest + n, n) > 0;
}

// ============================================================================

#if NCRYPTO_USE_BORINGSSL
HMACCtxPointer::HMACCtxPointer() : ctx_(nullptr) {}

HMACCtxPointer::HMACCtxPointer(HMAC_CTX* ctx) : ctx_(ctx) {}

HMACCtxPointer::HMACCtxPointer(HMACCtxPointer&& other) noexcept
    : ctx_(other.release()) {}

HMACCtxPointer& HMACCtxPointer::operator=(HMACCtxPointer&& other) noexcept {
  ctx_.reset(other.release());
  return *this;
}

HMACCtxPointer::~HMACCtxPointer() {
  reset();
}

void HMACCtxPointer::reset(HMAC_CTX* ctx) {
  ctx_.reset(ctx);
}

HMAC_CTX* HMACCtxPointer::release() {
  return ctx_.release();
}

bool HMACCtxPointer::init(const Buffer<const void>& buf, const Digest& md) {
  if (!ctx_) return false;
  const EVP_MD* md_ptr = md;
  return HMAC_Init_ex(ctx_.get(), buf.data, buf.len, md_ptr, nullptr) == 1;
}

bool HMACCtxPointer::update(const Buffer<const void>& buf) {
  if (!ctx_) return false;
  return HMAC_Update(ctx_.get(),
                     static_cast<const unsigned char*>(buf.data),
                     buf.len) == 1;
}

DataPointer HMACCtxPointer::digest() {
  auto data = DataPointer::Alloc(EVP_MAX_MD_SIZE);
  if (!data) return {};
  Buffer<void> buf = data;
  if (!digestInto(&buf)) return {};
  return data.resize(buf.len);
}

bool HMACCtxPointer::digestInto(Buffer<void>* buf) {
  if (!ctx_) return false;

  unsigned int len = buf->len;
  if (!HMAC_Final(ctx_.get(), static_cast<unsigned char*>(buf->data), &len)) {
    return false;
  }
  buf->len = len;
  return true;
}

HMACCtxPointer HMACCtxPointer::New() {
  return HMACCtxPointer(HMAC_CTX_new());
}
#endif  // NCRYPTO_USE_BORINGSSL

#if OPENSSL_WITH_EVP_MAC
EVPMacPointer::EVPMacPointer(EVP_MAC* mac) : mac_(mac) {}

EVPMacPointer::EVPMacPointer(EVPMacPointer&& other) noexcept
    : mac_(std::move(other.mac_)) {}

EVPMacPointer& EVPMacPointer::operator=(EVPMacPointer&& other) noexcept {
  if (this == &other) return *this;
  mac_ = std::move(other.mac_);
  return *this;
}

EVPMacPointer::~EVPMacPointer() {
  mac_.reset();
}

void EVPMacPointer::reset(EVP_MAC* mac) {
  mac_.reset(mac);
}

EVP_MAC* EVPMacPointer::release() {
  return mac_.release();
}

EVPMacPointer EVPMacPointer::Fetch(const char* algorithm) {
  return EVPMacPointer(EVP_MAC_fetch(nullptr, algorithm, nullptr));
}

MacKind MacCache::GetKind(EVP_MAC* mac) {
  if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_HMAC)) return MacKind::kHmac;
  if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_CMAC)) return MacKind::kCmac;
  if (EVP_MAC_is_a(mac, OSSL_MAC_NAME_GMAC)) return MacKind::kGmac;
  return MacKind::kOther;
}

MacCache::Result MacCache::lookup(const char* name, uint64_t generation) const {
  if (generation_ != generation || name == nullptr) return {};
  const auto it = aliases_.find(name);
  if (it == aliases_.end()) return {};
  return lookup(it->second, generation);
}

MacCache::Result MacCache::insert(const char* name,
                                  EVPMacPointer&& mac,
                                  uint64_t generation) {
  if (generation_ != generation || generation != getFipsStateGeneration() ||
      name == nullptr || mac == nullptr) {
    return {};
  }

  const char* canonical_name = EVP_MAC_get0_name(mac.get());
  const OSSL_PROVIDER* provider = EVP_MAC_get0_provider(mac.get());
  if (canonical_name == nullptr || provider == nullptr) return {};

  for (size_t index = 0; index < macs_.size(); index++) {
    EVP_MAC* cached = macs_[index].mac.get();
    if (cached == nullptr) continue;
    const char* cached_name = EVP_MAC_get0_name(cached);
    if (EVP_MAC_get0_provider(cached) == provider && cached_name != nullptr &&
        CaseInsensitiveNameEqual()(cached_name, canonical_name)) {
      if (generation != getFipsStateGeneration()) return {};
      const int32_t id = static_cast<int32_t>(first_id_ + index);
      aliases_.insert_or_assign(name, id);
      return {cached, id, macs_[index].kind};
    }
  }

  if (next_id_ == UINT32_MAX) return {};

  std::vector<std::string> aliases;
  {
    MarkPopErrorOnReturn mark_pop_error_on_return;
    if (EVP_MAC_names_do_all(mac.get(), PushAlgorithmAlias, &aliases) != 1) {
      return {};
    }
  }
  if (generation != getFipsStateGeneration()) return {};

  const MacKind kind = GetKind(mac.get());
  macs_.push_back({std::move(mac), kind});
  const int32_t id = static_cast<int32_t>(next_id_++);
  const size_t index = macs_.size() - 1;

  for (const std::string& alias : aliases) aliases_.emplace(alias, id);
  aliases_.insert_or_assign(name, id);

  return {macs_[index].mac.get(), id, kind};
}

void MacCache::reset(uint64_t generation) {
  if (generation_ == generation) return;
  aliases_.clear();
  macs_.clear();
  first_id_ = next_id_;
  generation_ = generation;
}

const MacCache::AliasMap& MacCache::aliases() const {
  return aliases_;
}

EVPMacCtxPointer::EVPMacCtxPointer(EVP_MAC_CTX* ctx) : ctx_(ctx) {}

EVPMacCtxPointer::EVPMacCtxPointer(EVPMacCtxPointer&& other) noexcept
    : ctx_(std::move(other.ctx_)) {}

EVPMacCtxPointer& EVPMacCtxPointer::operator=(
    EVPMacCtxPointer&& other) noexcept {
  if (this == &other) return *this;
  ctx_ = std::move(other.ctx_);
  return *this;
}

EVPMacCtxPointer::~EVPMacCtxPointer() {
  ctx_.reset();
}

void EVPMacCtxPointer::reset(EVP_MAC_CTX* ctx) {
  ctx_.reset(ctx);
}

EVP_MAC_CTX* EVPMacCtxPointer::release() {
  return ctx_.release();
}

bool EVPMacCtxPointer::init(const Buffer<const void>& key,
                            const OSSL_PARAM* params) {
  if (!ctx_) return false;

  static constexpr unsigned char kEmptyKey = 0;
  const unsigned char* key_data = static_cast<const unsigned char*>(key.data);
  if (key_data == nullptr) {
    if (key.len != 0) return false;
    key_data = &kEmptyKey;
  }

  return EVP_MAC_init(ctx_.get(), key_data, key.len, params) == 1;
}

bool EVPMacCtxPointer::update(const Buffer<const void>& data) {
  if (!ctx_) return false;
  if (data.len == 0) return true;
  if (data.data == nullptr) return false;
  return EVP_MAC_update(ctx_.get(),
                        static_cast<const unsigned char*>(data.data),
                        data.len) == 1;
}

size_t EVPMacCtxPointer::getSize() const {
  return ctx_ ? EVP_MAC_CTX_get_mac_size(ctx_.get()) : 0;
}

const OSSL_PARAM* EVPMacCtxPointer::getSettableParams() const {
  return ctx_ ? EVP_MAC_CTX_settable_params(ctx_.get()) : nullptr;
}

DataPointer EVPMacCtxPointer::final(size_t length) {
  if (!ctx_) return {};

  // DataPointer uses a null allocation to represent failure. Retain a
  // one-byte allocation for a successful zero-length result while passing the
  // requested zero capacity to OpenSSL. A non-null output pointer is required
  // to actually finalize; nullptr only queries the output length.
  auto buf = DataPointer::Alloc(length == 0 ? 1 : length);
  if (!buf) return {};

  size_t result_len = length;
  if (EVP_MAC_final(ctx_.get(),
                    static_cast<unsigned char*>(buf.get()),
                    &result_len,
                    length) != 1) {
    return {};
  }
  if (result_len > length) return {};

  return buf.resize(result_len);
}

EVPMacCtxPointer EVPMacCtxPointer::New(EVP_MAC* mac) {
  if (!mac) return EVPMacCtxPointer();
  return EVPMacCtxPointer(EVP_MAC_CTX_new(mac));
}

HMACCtxPointer::HMACCtxPointer() = default;

HMACCtxPointer::HMACCtxPointer(EVPMacPointer&& mac, EVPMacCtxPointer&& ctx)
    : mac_(std::move(mac)), ctx_(std::move(ctx)) {}

HMACCtxPointer::HMACCtxPointer(HMACCtxPointer&& other) noexcept
    : mac_(std::move(other.mac_)),
      ctx_(std::move(other.ctx_)),
      md_(other.md_),
      md_size_(other.md_size_) {
  other.md_ = nullptr;
  other.md_size_ = 0;
}

HMACCtxPointer& HMACCtxPointer::operator=(HMACCtxPointer&& other) noexcept {
  if (this == &other) return *this;
  mac_ = std::move(other.mac_);
  ctx_ = std::move(other.ctx_);
  md_ = other.md_;
  md_size_ = other.md_size_;
  other.md_ = nullptr;
  other.md_size_ = 0;
  return *this;
}

HMACCtxPointer::~HMACCtxPointer() {
  reset();
}

void HMACCtxPointer::reset() {
  ctx_.reset();
  mac_.reset();
  md_ = nullptr;
  md_size_ = 0;
}

bool HMACCtxPointer::init(const Buffer<const void>& buf, const Digest& md) {
  if (!ctx_ || !md) return false;
  if (md.get() == md_) return ctx_.init(buf);

  const char* md_name = EVP_MD_get0_name(md);
  if (md_name == nullptr) return false;

  OSSL_PARAM params[] = {
      OSSL_PARAM_construct_utf8_string(
          OSSL_MAC_PARAM_DIGEST, const_cast<char*>(md_name), 0),
      OSSL_PARAM_construct_end(),
  };

  if (!ctx_.init(buf, params)) return false;
  md_ = md.get();
  md_size_ = md.size();
  return true;
}

bool HMACCtxPointer::update(const Buffer<const void>& buf) {
  if (!ctx_) return false;
  return ctx_.update(buf);
}

DataPointer HMACCtxPointer::digest() {
  if (md_size_ == 0) return {};
  auto data = DataPointer::Alloc(md_size_);
  if (!data) return {};
  Buffer<void> buf = data;
  if (!digestInto(&buf)) return {};
  return data.resize(buf.len);
}

bool HMACCtxPointer::digestInto(Buffer<void>* buf) {
  if (!ctx_) return false;

  size_t len = buf->len;
  if (EVP_MAC_final(
          ctx_.get(), static_cast<unsigned char*>(buf->data), &len, buf->len) !=
      1)
    return false;

  buf->len = len;
  return true;
}

HMACCtxPointer HMACCtxPointer::clone() const {
  if (!ctx_) return {};
  EVPMacCtxPointer ctx(EVP_MAC_CTX_dup(ctx_.get()));
  if (!ctx) return {};
  HMACCtxPointer result(EVPMacPointer(), std::move(ctx));
  result.md_ = md_;
  result.md_size_ = md_size_;
  return result;
}

HMACCtxPointer HMACCtxPointer::New() {
  auto mac = EVPMacPointer::Fetch(OSSL_MAC_NAME_HMAC);
  if (!mac) return {};

  auto ctx = EVPMacCtxPointer::New(mac.get());
  if (!ctx) return {};

  return HMACCtxPointer(std::move(mac), std::move(ctx));
}

HMACCtxPointer HMACCtxPointer::New(const Digest& md) {
  if (!md) return {};
  const char* md_name = EVP_MD_get0_name(md);
  if (md_name == nullptr) return {};

  auto result = New();
  if (!result) return {};

  OSSL_PARAM params[] = {
      OSSL_PARAM_construct_utf8_string(
          OSSL_MAC_PARAM_DIGEST, const_cast<char*>(md_name), 0),
      OSSL_PARAM_construct_end(),
  };
  if (EVP_MAC_CTX_set_params(result.ctx_.get(), params) != 1) return {};
  result.md_ = md.get();
  result.md_size_ = md.size();
  return result;
}
#endif  // OPENSSL_WITH_EVP_MAC

DataPointer hashDigest(const Buffer<const unsigned char>& buf,
                       const EVP_MD* md) {
  if (md == nullptr) return {};
  size_t md_len = EVP_MD_size(md);
  unsigned int result_size;
  auto data = DataPointer::Alloc(md_len);
  if (!data) return {};

  if (!EVP_Digest(buf.data,
                  buf.len,
                  reinterpret_cast<unsigned char*>(data.get()),
                  &result_size,
                  md,
                  nullptr)) {
    return {};
  }

  return data.resize(result_size);
}

DataPointer xofHashDigest(const Buffer<const unsigned char>& buf,
                          const EVP_MD* md,
                          size_t output_length) {
  if (md == nullptr) return {};

  EVPMDCtxPointer ctx = EVPMDCtxPointer::New();
  if (!ctx) return {};
  if (ctx.digestInit(md) != 1) {
    return {};
  }
  if (ctx.digestUpdate(Buffer<const void>{
          .data = buf.data,
          .len = buf.len,
      }) != 1) {
    return {};
  }
  return ctx.digestFinal(output_length);
}

// ============================================================================

X509Name::X509Name() : name_(nullptr), total_(0) {}

X509Name::X509Name(const X509_NAME* name)
    : name_(name), total_(name == nullptr ? 0 : X509_NAME_entry_count(name)) {}

X509Name::Iterator::Iterator(const X509Name& name, int pos)
    : name_(name), loc_(pos) {}

X509Name::Iterator& X509Name::Iterator::operator++() {
  ++loc_;
  return *this;
}

X509Name::Iterator::operator bool() const {
  return loc_ < name_.total_;
}

bool X509Name::Iterator::operator==(const Iterator& other) const {
  return loc_ == other.loc_;
}

bool X509Name::Iterator::operator!=(const Iterator& other) const {
  return loc_ != other.loc_;
}

std::optional<X509Name::Entry> X509Name::Iterator::operator*() const {
  MarkPopErrorOnReturn mark_pop_error_on_return;
  if (loc_ == name_.total_) return std::nullopt;

  const X509_NAME_ENTRY* entry = X509_NAME_get_entry(name_, loc_);
  if (entry == nullptr) [[unlikely]]
    return std::nullopt;

  const ASN1_OBJECT* name = X509_NAME_ENTRY_get_object(entry);
  const ASN1_STRING* value = X509_NAME_ENTRY_get_data(entry);

  if (name == nullptr || value == nullptr) [[unlikely]] {
    return std::nullopt;
  }

  int nid = OBJ_obj2nid(name);
  std::string name_str;
  if (nid != NID_undef) {
    const char* short_name = OBJ_nid2sn(nid);
    if (short_name == nullptr) return std::nullopt;
    name_str = short_name;
  } else {
    auto name_text = ObjectToText(name, false);
    if (!name_text.has_value()) [[unlikely]] {
      return std::nullopt;
    }
    name_str = std::move(name_text.value());
  }

  switch (ASN1_STRING_type(value)) {
    case V_ASN1_UTF8STRING:
    case V_ASN1_NUMERICSTRING:
    case V_ASN1_PRINTABLESTRING:
    case V_ASN1_T61STRING:
    case V_ASN1_IA5STRING:
    case V_ASN1_UTCTIME:
    case V_ASN1_GENERALIZEDTIME:
    case V_ASN1_ISO64STRING:
    case V_ASN1_UNIVERSALSTRING:
    case V_ASN1_BMPSTRING:
      break;
    default:
      return Entry{std::move(name_str), std::nullopt};
  }

  unsigned char* value_str = nullptr;
  int value_str_size = ASN1_STRING_to_UTF8(&value_str, value);
  DataPointer value_data(
      value_str, value_str_size > 0 ? static_cast<size_t>(value_str_size) : 0);
  if (value_str_size < 0) [[unlikely]] {
    return std::nullopt;
  }

  std::string out;
  if (value_str_size > 0) {
    if (!value_data) return std::nullopt;
    out.assign(reinterpret_cast<const char*>(value_data.get()),
               static_cast<size_t>(value_str_size));
  }

  return Entry{std::move(name_str), std::move(out)};
}

// ============================================================================

#if NCRYPTO_USE_OPENSSL_PROVIDER
Dsa::Dsa() : dsa_(false) {}

Dsa::Dsa(const EVP_PKEY* pkey) : Dsa() {
  if (!EVPKeyPointer::isA(pkey, KeyAlgorithm::DSA)) return;
  if (!GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_P, &p_) ||
      !GetPKeyBnParam(pkey, OSSL_PKEY_PARAM_FFC_Q, &q_)) {
    return;
  }
  dsa_ = true;
}
#elif NCRYPTO_USE_BORINGSSL
Dsa::Dsa() : dsa_(nullptr) {}
Dsa::Dsa(OSSL3_CONST DSA* dsa) : dsa_(dsa) {}
#else
#error "Unsupported crypto backend"
#endif

const BIGNUM* Dsa::getP() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!dsa_) return nullptr;
  return p_.get();
#elif NCRYPTO_USE_BORINGSSL
  if (dsa_ == nullptr) return nullptr;
  const BIGNUM* p;
  DSA_get0_pqg(dsa_, &p, nullptr, nullptr);
  return p;
#else
#error "Unsupported crypto backend"
#endif
}

const BIGNUM* Dsa::getQ() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!dsa_) return nullptr;
  return q_.get();
#elif NCRYPTO_USE_BORINGSSL
  if (dsa_ == nullptr) return nullptr;
  const BIGNUM* q;
  DSA_get0_pqg(dsa_, nullptr, &q, nullptr);
  return q;
#else
#error "Unsupported crypto backend"
#endif
}

size_t Dsa::getModulusLength() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!dsa_) return 0;
#elif NCRYPTO_USE_BORINGSSL
  if (dsa_ == nullptr) return 0;
#else
#error "Unsupported crypto backend"
#endif
  return BignumPointer::GetBitCount(getP());
}

size_t Dsa::getDivisorLength() const {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!dsa_) return 0;
#elif NCRYPTO_USE_BORINGSSL
  if (dsa_ == nullptr) return 0;
#else
#error "Unsupported crypto backend"
#endif
  return BignumPointer::GetBitCount(getQ());
}

// ============================================================================

size_t Digest::size() const {
  if (md_ == nullptr) return 0;
  return EVP_MD_size(md_);
}

#if NCRYPTO_USE_OPENSSL_PROVIDER
Digest::Digest(DeleteFnPtr<EVP_MD, EVP_MD_free> md)
    : md_(md.get()), fetched_md_(std::move(md)) {}
#endif

Digest::Digest(const Digest& other) : md_(other.md_) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (other.fetched_md_ != nullptr) {
    if (EVP_MD_up_ref(other.fetched_md_.get()) == 1) {
      fetched_md_.reset(other.fetched_md_.get());
    } else {
      md_ = nullptr;
    }
  }
#endif
}

Digest& Digest::operator=(const Digest& other) {
  if (this == &other) return *this;
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (other.fetched_md_ != nullptr) {
    if (EVP_MD_up_ref(other.fetched_md_.get()) == 1) {
      fetched_md_.reset(other.fetched_md_.get());
    } else {
      fetched_md_.reset();
      md_ = nullptr;
      return *this;
    }
  } else {
    fetched_md_.reset();
  }
#endif
  md_ = other.md_;
  return *this;
}

const Digest Digest::MD5 = Digest(EVP_md5());
const Digest Digest::SHA1 = Digest(EVP_sha1());
const Digest Digest::SHA256 = Digest(EVP_sha256());
const Digest Digest::SHA384 = Digest(EVP_sha384());
const Digest Digest::SHA512 = Digest(EVP_sha512());

#if NCRYPTO_USE_OPENSSL_PROVIDER
namespace {
bool IsSupportedDigest(const EVP_MD* md) {
  if (md == nullptr || EVP_MD_is_a(md, "NULL")) return false;

  // OpenSSL currently crashes when ML-DSA-MU finalizes an empty input. Keep it
  // unavailable until the provider implementation is fixed.
  // https://github.com/openssl/openssl/issues/32445
  if (EVP_MD_is_a(md, "ML-DSA-MU")) return false;

  return true;
}
}  // namespace
#endif

const Digest Digest::FromName(const char* name) {
  const EVP_MD* md = ncrypto::getDigestByName(name);
  if (md != nullptr) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
    if (md == EVP_md_null()) return Digest();
#endif
    return Digest(md);
  }

  return Fetch(name);
}

const Digest Digest::Fetch(const char* name) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  MarkPopErrorOnReturn mark_pop_error_on_return;
  DeleteFnPtr<EVP_MD, EVP_MD_free> fetched(
      EVP_MD_fetch(nullptr, name, nullptr));
  if (IsSupportedDigest(fetched.get())) {
    return Digest(std::move(fetched));
  }
#endif

  return Digest();
}

const Digest Digest::Ref(const EVP_MD* md) {
#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (md != nullptr && EVP_MD_get0_provider(md) != nullptr) {
    if (EVP_MD_up_ref(const_cast<EVP_MD*>(md)) != 1) return Digest();
    return Digest(DeleteFnPtr<EVP_MD, EVP_MD_free>(const_cast<EVP_MD*>(md)));
  }
#endif
  return Digest(md);
}

// ============================================================================
// KEM Implementation
#if OPENSSL_WITH_KEM
#if NCRYPTO_USE_OPENSSL_PROVIDER
bool KEM::SetOperationParameter(EVP_PKEY_CTX* ctx, const EVPKeyPointer& key) {
  const OSSL_PARAM* settable = EVP_PKEY_CTX_settable_params(ctx);
  if (settable == nullptr ||
      OSSL_PARAM_locate_const(settable, "operation") == nullptr)
    return true;

  const char* operation = nullptr;
  if (key.isA(KeyAlgorithm::RSA)) {
    operation = "RSASVE";
  } else if (key.isA(KeyAlgorithm::EC) || key.isA(KeyAlgorithm::X25519) ||
             key.isA(KeyAlgorithm::X448)) {
    operation = "DHKEM";
  }
  if (operation == nullptr) return true;
  OSSL_PARAM params[] = {
      OSSL_PARAM_utf8_string("operation", const_cast<char*>(operation), 0),
      OSSL_PARAM_END};
  return EVP_PKEY_CTX_set_params(ctx, params) > 0;
}
#endif

std::optional<KEM::EncapsulateResult> KEM::Encapsulate(
    const EVPKeyPointer& public_key) {
  ClearErrorOnReturn clear_error_on_return;

  auto ctx = public_key.newCtx();
  if (!ctx) return std::nullopt;

  if (EVP_PKEY_encapsulate_init(ctx.get(), nullptr) <= 0) {
    return std::nullopt;
  }

#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!SetOperationParameter(ctx.get(), public_key)) {
    return std::nullopt;
  }
#endif

  // Determine output buffer sizes
  size_t ciphertext_len = 0;
  size_t shared_key_len = 0;

  if (EVP_PKEY_encapsulate(
          ctx.get(), nullptr, &ciphertext_len, nullptr, &shared_key_len) <= 0) {
    return std::nullopt;
  }

  auto ciphertext = DataPointer::Alloc(ciphertext_len);
  auto shared_key = DataPointer::Alloc(shared_key_len);
  if (!ciphertext || !shared_key) return std::nullopt;

  if (EVP_PKEY_encapsulate(ctx.get(),
                           static_cast<unsigned char*>(ciphertext.get()),
                           &ciphertext_len,
                           static_cast<unsigned char*>(shared_key.get()),
                           &shared_key_len) <= 0) {
    return std::nullopt;
  }

  return EncapsulateResult(std::move(ciphertext), std::move(shared_key));
}

DataPointer KEM::Decapsulate(const EVPKeyPointer& private_key,
                             const Buffer<const void>& ciphertext) {
  ClearErrorOnReturn clear_error_on_return;

  auto ctx = private_key.newCtx();
  if (!ctx) return {};

  if (EVP_PKEY_decapsulate_init(ctx.get(), nullptr) <= 0) {
    return {};
  }

#if NCRYPTO_USE_OPENSSL_PROVIDER
  if (!SetOperationParameter(ctx.get(), private_key)) {
    return {};
  }
#endif

  // First pass: determine shared secret size
  size_t shared_key_len = 0;
  if (EVP_PKEY_decapsulate(ctx.get(),
                           nullptr,
                           &shared_key_len,
                           static_cast<const unsigned char*>(ciphertext.data),
                           ciphertext.len) <= 0) {
    return {};
  }

  auto shared_key = DataPointer::Alloc(shared_key_len);
  if (!shared_key) return {};

  if (EVP_PKEY_decapsulate(ctx.get(),
                           static_cast<unsigned char*>(shared_key.get()),
                           &shared_key_len,
                           static_cast<const unsigned char*>(ciphertext.data),
                           ciphertext.len) <= 0) {
    return {};
  }

  return shared_key;
}

#endif  // OPENSSL_WITH_KEM

}  // namespace ncrypto