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 →