Skip to content

Digital Signature — C++ source

Sign messages with Ed25519 or ECDSA and verify signatures. Prove authorship without revealing your private key.

This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// Digital Signature — Ed25519 / ECDSA P-256 message signing and verification
// via OpenSSL EVP.
//
// Language: C++17 (standard library + OpenSSL EVP)
// Ported from src/lib/digital-signature.ts (the canonical TypeScript
// implementation, which uses the browser's Web Crypto API).
// display source — part of CosmoDev's polyglot tool pages.
//
// Keys, messages and signatures never leave the process — the C++ analogue of
// the TS guarantee that everything happens client-side.
//
// Key encoding: keys are exchanged as hex of the standard DER structures -
// SPKI (SubjectPublicKeyInfo) for public keys, PKCS8 for private keys - so
// they interoperate with OpenSSL, SSH, JOSE tooling and any other Web Crypto
// implementation. Signatures are hex of the raw signature bytes: 64 bytes for
// both algorithms (Ed25519 R||S; ECDSA P-256 in IEEE P1363 r||s layout, NOT
// ASN.1 DER — OpenSSL emits DER, so this port converts, keeping the same wire
// format as the TS reference). ECDSA signs the SHA-256 digest of the message.
//
// Build: c++ -std=c++17 digital-signature.cpp -lcrypto

#include <algorithm>
#include <cstdint>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

#include <openssl/evp.h>

namespace digitalsignature {

using Bytes = std::vector<uint8_t>;

enum class SignAlgorithm { Ed25519, ECDSAP256 };

struct KeyPair {
  std::string publicKey; // hex-encoded SPKI (SubjectPublicKeyInfo) public key — safe to share
  std::string privateKey; // hex-encoded PKCS8 private key — keep it secret, it IS the identity
};

// ── small OpenSSL RAII helpers ─────────────────────────────────────────────

struct Deleter {
  void operator()(EVP_MD_CTX* ctx) const { EVP_MD_CTX_free(ctx); }
  void operator()(EVP_PKEY_CTX* ctx) const { EVP_PKEY_CTX_free(ctx); }
  void operator()(EVP_PKEY* key) const { EVP_PKEY_free(key); }
};
using MDContext = std::unique_ptr<EVP_MD_CTX, Deleter>;
using PKEYContext = std::unique_ptr<EVP_PKEY_CTX, Deleter>;
using PKey = std::unique_ptr<EVP_PKEY, Deleter>;

// ── hex codec ──────────────────────────────────────────────────────────────

static std::string toHex(const Bytes& bytes) {
  static const char* HEX = "0123456789abcdef";
  std::string out;
  out.reserve(bytes.size() * 2);
  for (uint8_t b : bytes) {
    out += HEX[b >> 4];
    out += HEX[b & 0x0f];
  }
  return out;
}

static Bytes hexToBytes(const std::string& hex, const std::string& what) {
  if (hex.empty() || hex.size() % 2 != 0) {
    throw std::runtime_error(what + " must be a non-empty hex string (pairs of 0-9 / a-f digits).");
  }
  for (char c : hex) {
    const bool ok = (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
    if (!ok) {
      throw std::runtime_error(what + " must be a non-empty hex string (pairs of 0-9 / a-f digits).");
    }
  }
  Bytes out(hex.size() / 2);
  for (size_t i = 0; i < out.size(); i++) {
    out[i] = static_cast<uint8_t>(std::stoul(hex.substr(i * 2, 2), nullptr, 16));
  }
  return out;
}

// ── algorithm plumbing ─────────────────────────────────────────────────────

static void assertAlgorithm(SignAlgorithm algorithm) {
  if (algorithm != SignAlgorithm::Ed25519 && algorithm != SignAlgorithm::ECDSAP256) {
    throw std::runtime_error("Unknown algorithm. Use \"ed25519\" or \"ecdsa-p256\".");
  }
}

static std::string label(SignAlgorithm algorithm) {
  return algorithm == SignAlgorithm::Ed25519 ? "Ed25519" : "ECDSA P-256";
}

/** One-shot Ed25519 digest-sign: raw 64-byte R||S signature. */
static Bytes ed25519Sign(EVP_PKEY* key, const Bytes& message) {
  MDContext ctx(EVP_MD_CTX_new());
  size_t len = 64;
  Bytes signature(len);
  if (!ctx || EVP_DigestSignInit(ctx.get(), nullptr, nullptr, nullptr, key) != 1 ||
      EVP_DigestSign(ctx.get(), signature.data(), &len, message.data(), message.size()) != 1) {
    throw std::runtime_error("Ed25519 signing failed.");
  }
  signature.resize(len);
  return signature;
}

/** One-shot Ed25519 digest-verify of a raw 64-byte R||S signature. */
static bool ed25519Verify(EVP_PKEY* key, const Bytes& message, const Bytes& signature) {
  MDContext ctx(EVP_MD_CTX_new());
  return ctx && EVP_DigestVerifyInit(ctx.get(), nullptr, nullptr, nullptr, key) == 1 &&
         EVP_DigestVerify(ctx.get(), signature.data(), signature.size(), message.data(),
                          message.size()) == 1;
}

// ── ECDSA DER ↔ IEEE P1363 conversion ──────────────────────────────────────

/**
 * Read one DER INTEGER (tag 02) at `pos`, strip its sign padding, and return
 * the big-endian bytes. Advances `pos` past the TLV.
 */
static Bytes readDerInteger(const Bytes& der, size_t& pos) {
  if (pos + 2 > der.size() || der[pos] != 0x02) {
    throw std::runtime_error("Malformed ECDSA signature DER.");
  }
  const uint8_t len = der[pos + 1];
  if (len == 0 || pos + 2 + len > der.size()) {
    throw std::runtime_error("Malformed ECDSA signature DER.");
  }
  size_t start = pos + 2;
  size_t length = len;
  if (der[start] == 0x00 && length > 1) { // positive-sign padding byte
    start++;
    length--;
  }
  pos += 2 + len;
  return Bytes(der.begin() + start, der.begin() + start + length);
}

static void appendDerInteger(Bytes& out, const Bytes& value) {
  out.push_back(0x02);
  size_t length = value.size();
  const bool needsSignPad = !value.empty() && (value[0] & 0x80) != 0;
  if (needsSignPad) length++;
  out.push_back(static_cast<uint8_t>(length));
  if (needsSignPad) out.push_back(0x00);
  out.insert(out.end(), value.begin(), value.end());
}

/**
 * OpenSSL emits ECDSA signatures as ASN.1 DER SEQUENCE(INTEGER r, INTEGER s);
 * Web Crypto — and the TS reference — use the fixed-width IEEE P1363 r||s
 * layout. Convert DER → padded r||s.
 */
static Bytes ecdsaDerToP1363(const Bytes& der, size_t halfLength) {
  size_t pos = 0;
  const Bytes r = readDerInteger(der, pos);
  const Bytes s = readDerInteger(der, pos);
  Bytes out(2 * halfLength, 0);
  std::copy(r.rbegin(), r.rend(), out.rend() - halfLength);
  std::copy(s.rbegin(), s.rend(), out.rend());
  return out;
}

/** P1363 r||s → DER SEQUENCE, the inverse of ecdsaDerToP1363. */
static Bytes ecdsaP1363ToDer(const Bytes& p1363) {
  const size_t half = p1363.size() / 2;
  Bytes r(p1363.begin(), p1363.begin() + half);
  Bytes s(p1363.begin() + half, p1363.end());
  Bytes content;
  appendDerInteger(content, r);
  appendDerInteger(content, s);
  Bytes out{0x30, static_cast<uint8_t>(content.size())};
  out.insert(out.end(), content.begin(), content.end());
  return out;
}

// ── key generation / import / export ───────────────────────────────────────

/** Generate a fresh signing key pair, exported as hex SPKI / PKCS8 keys. */
KeyPair generateKeyPair(SignAlgorithm algorithm) {
  assertAlgorithm(algorithm);

  EVP_PKEY* raw = nullptr;
  PKEYContext ctx(algorithm == SignAlgorithm::Ed25519
                      ? EVP_PKEY_CTX_new_id(EVP_PKEY_ED25519, nullptr)
                      : EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr));
  if (!ctx || EVP_PKEY_keygen_init(ctx.get()) != 1) {
    throw std::runtime_error(algorithm == SignAlgorithm::Ed25519
                                 ? "Ed25519 is not supported by this OpenSSL build. Switch to "
                                   "ECDSA P-256 or update the library."
                                 : "ECDSA P-256 key generation failed.");
  }
  if (algorithm == SignAlgorithm::ECDSAP256) {
    if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx.get(), NID_X9_62_prime256v1) <= 0) {
      throw std::runtime_error("ECDSA P-256 key generation failed.");
    }
  }
  if (EVP_PKEY_keygen(ctx.get(), &raw) != 1) {
    throw std::runtime_error(algorithm == SignAlgorithm::Ed25519
                                 ? "Ed25519 key generation failed."
                                 : "ECDSA P-256 key generation failed.");
  }
  const PKey key(raw);

  // SPKI export
  int len = i2d_PUBKEY(key.get(), nullptr);
  if (len <= 0) throw std::runtime_error("Public key export failed.");
  Bytes spki(static_cast<size_t>(len));
  uint8_t* cursor = spki.data();
  if (i2d_PUBKEY(key.get(), &cursor) <= 0) throw std::runtime_error("Public key export failed.");

  // PKCS8 export
  const PKCS8_PRIV_KEY_INFO* pkcs8Info = EVP_PKEY_get0_PKCS8(key.get());
  len = i2d_PKCS8_PRIV_KEY_INFO(pkcs8Info, nullptr);
  if (len <= 0) throw std::runtime_error("Private key export failed.");
  Bytes pkcs8(static_cast<size_t>(len));
  cursor = pkcs8.data();
  if (i2d_PKCS8_PRIV_KEY_INFO(pkcs8Info, &cursor) <= 0) {
    throw std::runtime_error("Private key export failed.");
  }

  return {toHex(spki), toHex(pkcs8)};
}

/** Import a hex PKCS8 private key for `algorithm`. */
static PKey importPrivateKey(const std::string& privateKeyHex, SignAlgorithm algorithm) {
  const Bytes bytes = hexToBytes(privateKeyHex, "Private key");
  const uint8_t* cursor = bytes.data();
  PKCS8_PRIV_KEY_INFO* info =
      d2i_PKCS8_PRIV_KEY_INFO(nullptr, &cursor, static_cast<long>(bytes.size()));
  if (info == nullptr) {
    throw std::runtime_error("Invalid private key for " + label(algorithm) +
                             " (expected hex PKCS8).");
  }
  EVP_PKEY* key = EVP_PKCS82PKEY(info);
  PKCS8_PRIV_KEY_INFO_free(info);
  if (key == nullptr) {
    throw std::runtime_error("Invalid private key for " + label(algorithm) +
                             " (expected hex PKCS8).");
  }
  return PKey(key);
}

/** Import a hex SPKI public key for `algorithm`. */
static PKey importPublicKey(const std::string& publicKeyHex, SignAlgorithm algorithm) {
  const Bytes bytes = hexToBytes(publicKeyHex, "Public key");
  const uint8_t* cursor = bytes.data();
  EVP_PKEY* key = d2i_PUBKEY(nullptr, &cursor, static_cast<long>(bytes.size()));
  if (key == nullptr) {
    throw std::runtime_error("Invalid public key for " + label(algorithm) +
                             " (expected hex SPKI).");
  }
  return PKey(key);
}

// ── sign / verify ──────────────────────────────────────────────────────────

/** Sign `message` with a hex PKCS8 private key. Returns the hex signature. */
std::string signMessage(const std::string& message, const std::string& privateKeyHex,
                        SignAlgorithm algorithm) {
  assertAlgorithm(algorithm);
  if (message.empty()) throw std::runtime_error("Message must not be empty.");
  const PKey key = importPrivateKey(privateKeyHex, algorithm);
  const Bytes msg(message.begin(), message.end());

  if (algorithm == SignAlgorithm::Ed25519) {
    return toHex(ed25519Sign(key.get(), msg));
  }
  // ECDSA P-256 + SHA-256: OpenSSL gives DER; convert to the P1363 r||s
  // layout the TS reference (and JOSE tooling) expect — always 64 bytes.
  MDContext ctx(EVP_MD_CTX_new());
  size_t len = 0;
  if (!ctx || EVP_DigestSignInit(ctx.get(), nullptr, EVP_sha256(), nullptr, key.get()) != 1 ||
      EVP_DigestSign(ctx.get(), nullptr, &len, msg.data(), msg.size()) != 1) {
    throw std::runtime_error("ECDSA P-256 signing failed.");
  }
  Bytes der(len);
  if (EVP_DigestSign(ctx.get(), der.data(), &len, msg.data(), msg.size()) != 1) {
    throw std::runtime_error("ECDSA P-256 signing failed.");
  }
  der.resize(len);
  return toHex(ecdsaDerToP1363(der, 32));
}

/**
 * Verify `signatureHex` against `message` with a hex SPKI public key.
 * Returns false when the signature, message or key simply do not match;
 * throws only for malformed input (non-hex / wrong key type).
 */
bool verifySignature(const std::string& message, const std::string& signatureHex,
                     const std::string& publicKeyHex, SignAlgorithm algorithm) {
  assertAlgorithm(algorithm);
  const Bytes signature = hexToBytes(signatureHex, "Signature");
  const PKey key = importPublicKey(publicKeyHex, algorithm);
  const Bytes msg(message.begin(), message.end());

  if (algorithm == SignAlgorithm::Ed25519) {
    return ed25519Verify(key.get(), msg, signature);
  }
  const Bytes der = ecdsaP1363ToDer(signature);
  MDContext ctx(EVP_MD_CTX_new());
  return ctx && EVP_DigestVerifyInit(ctx.get(), nullptr, EVP_sha256(), nullptr, key.get()) == 1 &&
         EVP_DigestVerify(ctx.get(), der.data(), der.size(), msg.data(), msg.size()) == 1;
}

} // namespace digitalsignature

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →