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.
*
* Language: C (C11, standard library + OpenSSL 3.x libcrypto — C has no crypto
* in its standard library; libcrypto is the de-facto native choice)
* Source: CosmoDev polyglot showcase port of the Digital Signature tool,
* ported from src/lib/digital-signature.ts (the canonical TypeScript
* implementation).
* License: display source — part of CosmoDev's polyglot tool pages.
*
* Key encoding matches the TS reference exactly: 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 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). ECDSA signs the SHA-256 digest.
*
* The P1363 detail is the one place C has to work harder than the browser:
* OpenSSL emits/consumes ASN.1 DER ECDSA signatures, so this port converts
* both ways (p1363_from_der / der_from_p1363) to stay wire-compatible with
* Web Crypto.
*
* Build: cc -std=c11 dsig.c -lcrypto
*/
#include <stdarg.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/bn.h>
#include <openssl/ec.h>
#include <openssl/ecdsa.h>
#include <openssl/evp.h>
#include <openssl/x509.h>
/* --------------------------------------------------------------- algorithm --- */
typedef enum {
SIGN_ED25519,
SIGN_ECDSA_P256,
SIGN_UNKNOWN
} sign_algorithm;
/* P-256 field elements are 32 bytes each, so r||s is always 64 bytes. */
#define P256_COORD_LEN 32u
#define P1363_SIG_LEN (P256_COORD_LEN * 2u)
/* Parse the algorithm id used by the TS API ("ed25519" / "ecdsa-p256"). */
sign_algorithm sign_algorithm_parse(const char *name)
{
if (name == NULL) {
return SIGN_UNKNOWN;
}
if (strcmp(name, "ed25519") == 0) {
return SIGN_ED25519;
}
if (strcmp(name, "ecdsa-p256") == 0) {
return SIGN_ECDSA_P256;
}
return SIGN_UNKNOWN;
}
static const char *algorithm_label(sign_algorithm alg)
{
return alg == SIGN_ED25519 ? "Ed25519" : "ECDSA P-256";
}
/* ------------------------------------------------------------------ result --- */
/* Hex-encoded key pair. Both strings are heap-allocated; free with
* sign_key_pair_free(). `private_key` IS the identity — keep it secret. */
typedef struct {
char *public_key; /* hex SPKI */
char *private_key; /* hex PKCS8 */
} sign_key_pair;
/* Every public entry point reports failure the same way: ok == false plus a
* caller-facing message, mirroring the TS reference's thrown Errors. */
typedef struct {
bool ok;
char error[192];
} sign_status;
static sign_status status_ok(void)
{
sign_status s = { .ok = true };
s.error[0] = '\0';
return s;
}
static sign_status status_fail(const char *fmt, ...)
{
sign_status s = { .ok = false };
va_list ap;
va_start(ap, fmt);
vsnprintf(s.error, sizeof s.error, fmt, ap);
va_end(ap);
return s;
}
void sign_key_pair_free(sign_key_pair *pair)
{
if (pair == NULL) {
return;
}
free(pair->public_key);
if (pair->private_key != NULL) {
OPENSSL_cleanse(pair->private_key, strlen(pair->private_key));
free(pair->private_key);
}
pair->public_key = NULL;
pair->private_key = NULL;
}
/* --------------------------------------------------------------- hex codec --- */
static int hex_nibble(char c)
{
if (c >= '0' && c <= '9') {
return c - '0';
}
if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
}
if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
}
return -1;
}
/*
* Decode a hex string into a fresh byte buffer. Mirrors the TS `HEX` guard:
* the input must be non-empty and an even number of hex digits, nothing else.
* Returns NULL on malformed input.
*/
static uint8_t *hex_to_bytes(const char *hex, size_t *out_len)
{
if (hex == NULL) {
return NULL;
}
size_t n = strlen(hex);
if (n == 0 || n % 2 != 0) {
return NULL;
}
uint8_t *out = malloc(n / 2);
if (out == NULL) {
return NULL;
}
for (size_t i = 0; i < n; i += 2) {
int hi = hex_nibble(hex[i]);
int lo = hex_nibble(hex[i + 1]);
if (hi < 0 || lo < 0) {
free(out);
return NULL;
}
out[i / 2] = (uint8_t)((hi << 4) | lo);
}
*out_len = n / 2;
return out;
}
/* Lowercase hex, zero-padded per byte — matches the TS toHex(). */
static char *bytes_to_hex(const uint8_t *bytes, size_t len)
{
char *out = malloc(len * 2 + 1);
if (out == NULL) {
return NULL;
}
static const char digits[] = "0123456789abcdef";
for (size_t i = 0; i < len; i++) {
out[i * 2] = digits[bytes[i] >> 4];
out[i * 2 + 1] = digits[bytes[i] & 0x0f];
}
out[len * 2] = '\0';
return out;
}
/* ----------------------------------------------------- ECDSA sig transcode --- */
/* DER ECDSA-Sig-Value -> fixed-width IEEE P1363 r||s (what Web Crypto uses). */
static bool p1363_from_der(const uint8_t *der, size_t der_len,
uint8_t out[P1363_SIG_LEN])
{
const uint8_t *p = der;
ECDSA_SIG *sig = d2i_ECDSA_SIG(NULL, &p, (long)der_len);
if (sig == NULL) {
return false;
}
const BIGNUM *r = ECDSA_SIG_get0_r(sig);
const BIGNUM *s = ECDSA_SIG_get0_s(sig);
memset(out, 0, P1363_SIG_LEN);
bool ok = BN_bn2binpad(r, out, (int)P256_COORD_LEN) == (int)P256_COORD_LEN &&
BN_bn2binpad(s, out + P256_COORD_LEN, (int)P256_COORD_LEN) ==
(int)P256_COORD_LEN;
ECDSA_SIG_free(sig);
return ok;
}
/* Fixed-width IEEE P1363 r||s -> DER ECDSA-Sig-Value (what OpenSSL verifies).
* On success *der is heap-allocated and must be OPENSSL_free()d. */
static bool der_from_p1363(const uint8_t sig64[P1363_SIG_LEN],
uint8_t **der, size_t *der_len)
{
BIGNUM *r = BN_bin2bn(sig64, (int)P256_COORD_LEN, NULL);
BIGNUM *s = BN_bin2bn(sig64 + P256_COORD_LEN, (int)P256_COORD_LEN, NULL);
if (r == NULL || s == NULL) {
BN_free(r);
BN_free(s);
return false;
}
ECDSA_SIG *sig = ECDSA_SIG_new();
if (sig == NULL) {
BN_free(r);
BN_free(s);
return false;
}
/* ECDSA_SIG_set0 takes ownership of r and s on success. */
if (ECDSA_SIG_set0(sig, r, s) != 1) {
BN_free(r);
BN_free(s);
ECDSA_SIG_free(sig);
return false;
}
uint8_t *buf = NULL;
int len = i2d_ECDSA_SIG(sig, &buf);
ECDSA_SIG_free(sig);
if (len <= 0) {
return false;
}
*der = buf;
*der_len = (size_t)len;
return true;
}
/* ------------------------------------------------------------- key import --- */
/* Import a hex PKCS8 private key. Returns NULL on malformed input. */
static EVP_PKEY *import_private_key(const char *hex, sign_algorithm alg)
{
size_t len = 0;
uint8_t *bytes = hex_to_bytes(hex, &len);
if (bytes == NULL) {
return NULL;
}
const uint8_t *p = bytes;
EVP_PKEY *key = d2i_PrivateKey_ex(EVP_PKEY_NONE, NULL, &p, (long)len,
NULL, NULL);
OPENSSL_cleanse(bytes, len);
free(bytes);
if (key == NULL) {
return NULL;
}
/* Reject a well-formed key of the wrong type — the TS reference surfaces
* this as "Invalid private key for <algorithm>". */
int want = (alg == SIGN_ED25519) ? EVP_PKEY_ED25519 : EVP_PKEY_EC;
if (EVP_PKEY_base_id(key) != want) {
EVP_PKEY_free(key);
return NULL;
}
return key;
}
/* Import a hex SPKI public key. Returns NULL on malformed input. */
static EVP_PKEY *import_public_key(const char *hex, sign_algorithm alg)
{
size_t len = 0;
uint8_t *bytes = hex_to_bytes(hex, &len);
if (bytes == NULL) {
return NULL;
}
const uint8_t *p = bytes;
EVP_PKEY *key = d2i_PUBKEY(NULL, &p, (long)len);
free(bytes);
if (key == NULL) {
return NULL;
}
int want = (alg == SIGN_ED25519) ? EVP_PKEY_ED25519 : EVP_PKEY_EC;
if (EVP_PKEY_base_id(key) != want) {
EVP_PKEY_free(key);
return NULL;
}
return key;
}
/* ------------------------------------------------------------- key export --- */
/* Generate a fresh signing key pair, exported as hex SPKI / PKCS8 keys. */
sign_status generate_key_pair(sign_algorithm alg, sign_key_pair *out)
{
if (out == NULL) {
return status_fail("Output parameter must not be NULL.");
}
out->public_key = NULL;
out->private_key = NULL;
if (alg != SIGN_ED25519 && alg != SIGN_ECDSA_P256) {
return status_fail("Unknown algorithm. Use \"ed25519\" or \"ecdsa-p256\".");
}
EVP_PKEY *key = NULL;
if (alg == SIGN_ED25519) {
key = EVP_PKEY_Q_keygen(NULL, NULL, "ED25519");
} else {
key = EVP_PKEY_Q_keygen(NULL, NULL, "EC", "P-256");
}
if (key == NULL) {
return status_fail(alg == SIGN_ED25519
? "Ed25519 is not supported by this OpenSSL build. "
"Switch to ECDSA P-256 or update OpenSSL."
: "ECDSA P-256 key generation failed.");
}
uint8_t *spki = NULL;
uint8_t *pkcs8 = NULL;
int spki_len = i2d_PUBKEY(key, &spki);
int pkcs8_len = i2d_PrivateKey(key, &pkcs8);
EVP_PKEY_free(key);
if (spki_len <= 0 || pkcs8_len <= 0) {
OPENSSL_free(spki);
OPENSSL_free(pkcs8);
return status_fail("Key export failed.");
}
out->public_key = bytes_to_hex(spki, (size_t)spki_len);
out->private_key = bytes_to_hex(pkcs8, (size_t)pkcs8_len);
OPENSSL_cleanse(pkcs8, (size_t)pkcs8_len);
OPENSSL_free(spki);
OPENSSL_free(pkcs8);
if (out->public_key == NULL || out->private_key == NULL) {
sign_key_pair_free(out);
return status_fail("Out of memory.");
}
return status_ok();
}
/* ----------------------------------------------------------------- signing --- */
/*
* Sign `message` with a hex PKCS8 private key. On success *signature_hex is a
* heap-allocated hex string (64 raw bytes -> 128 hex chars) owned by caller.
*/
sign_status sign_message(const char *message, const char *private_key_hex,
sign_algorithm alg, char **signature_hex)
{
if (signature_hex == NULL) {
return status_fail("Output parameter must not be NULL.");
}
*signature_hex = NULL;
if (alg != SIGN_ED25519 && alg != SIGN_ECDSA_P256) {
return status_fail("Unknown algorithm. Use \"ed25519\" or \"ecdsa-p256\".");
}
if (message == NULL || *message == '\0') {
return status_fail("Message must not be empty.");
}
EVP_PKEY *key = import_private_key(private_key_hex, alg);
if (key == NULL) {
return status_fail("Invalid private key for %s (expected hex PKCS8).",
algorithm_label(alg));
}
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (ctx == NULL) {
EVP_PKEY_free(key);
return status_fail("Out of memory.");
}
/* Ed25519 is a one-shot pure signature (no pre-hash); ECDSA pins SHA-256. */
const EVP_MD *md = (alg == SIGN_ED25519) ? NULL : EVP_sha256();
size_t msg_len = strlen(message);
uint8_t *raw = NULL;
size_t raw_len = 0;
bool ok = false;
if (EVP_DigestSignInit(ctx, NULL, md, NULL, key) == 1 &&
EVP_DigestSign(ctx, NULL, &raw_len, (const uint8_t *)message, msg_len) == 1) {
raw = malloc(raw_len);
ok = raw != NULL &&
EVP_DigestSign(ctx, raw, &raw_len, (const uint8_t *)message, msg_len) == 1;
}
EVP_MD_CTX_free(ctx);
EVP_PKEY_free(key);
if (!ok) {
free(raw);
return status_fail("Signing failed.");
}
if (alg == SIGN_ED25519) {
/* Already 64 raw bytes of R||S. */
*signature_hex = bytes_to_hex(raw, raw_len);
} else {
/* OpenSSL produced DER; Web Crypto expects fixed-width r||s. */
uint8_t p1363[P1363_SIG_LEN];
if (!p1363_from_der(raw, raw_len, p1363)) {
free(raw);
return status_fail("Signature encoding failed.");
}
*signature_hex = bytes_to_hex(p1363, sizeof p1363);
}
free(raw);
return *signature_hex == NULL ? status_fail("Out of memory.") : status_ok();
}
/* ------------------------------------------------------------ verification --- */
/*
* Verify `signature_hex` against `message` with a hex SPKI public key.
* Sets *valid to false when the signature, message or key simply do not match;
* returns ok == false only for malformed input (non-hex / wrong key type),
* mirroring the TS reference's "returns false vs throws" split.
*/
sign_status verify_signature(const char *message, const char *signature_hex,
const char *public_key_hex, sign_algorithm alg,
bool *valid)
{
if (valid == NULL) {
return status_fail("Output parameter must not be NULL.");
}
*valid = false;
if (alg != SIGN_ED25519 && alg != SIGN_ECDSA_P256) {
return status_fail("Unknown algorithm. Use \"ed25519\" or \"ecdsa-p256\".");
}
size_t sig_len = 0;
uint8_t *sig = hex_to_bytes(signature_hex, &sig_len);
if (sig == NULL) {
return status_fail("Signature must be a non-empty hex string "
"(pairs of 0-9 / a-f digits).");
}
EVP_PKEY *key = import_public_key(public_key_hex, alg);
if (key == NULL) {
free(sig);
return status_fail("Invalid public key for %s (expected hex SPKI).",
algorithm_label(alg));
}
/* A wrong-length signature is a mismatch, not malformed input — the TS
* reference likewise lets Web Crypto answer `false` here. */
uint8_t *der = NULL;
size_t der_len = 0;
const uint8_t *check = sig;
size_t check_len = sig_len;
if (alg == SIGN_ECDSA_P256) {
if (sig_len != P1363_SIG_LEN || !der_from_p1363(sig, &der, &der_len)) {
free(sig);
EVP_PKEY_free(key);
return status_ok(); /* *valid stays false */
}
check = der;
check_len = der_len;
}
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
if (ctx == NULL) {
OPENSSL_free(der);
free(sig);
EVP_PKEY_free(key);
return status_fail("Out of memory.");
}
const EVP_MD *md = (alg == SIGN_ED25519) ? NULL : EVP_sha256();
if (EVP_DigestVerifyInit(ctx, NULL, md, NULL, key) == 1) {
*valid = EVP_DigestVerify(ctx, check, check_len,
(const uint8_t *)(message == NULL ? "" : message),
message == NULL ? 0 : strlen(message)) == 1;
}
EVP_MD_CTX_free(ctx);
OPENSSL_free(der);
free(sig);
EVP_PKEY_free(key);
return status_ok();
}
/* -------------------------------------------------------------- demo main --- */
#ifdef DSIG_DEMO
static void round_trip(const char *alg_name)
{
sign_algorithm alg = sign_algorithm_parse(alg_name);
sign_key_pair kp = { NULL, NULL };
sign_status st = generate_key_pair(alg, &kp);
if (!st.ok) {
fprintf(stderr, "%s keygen: %s\n", alg_name, st.error);
return;
}
printf("%s public key (%zu hex chars)\n", alg_name, strlen(kp.public_key));
const char *msg = "the treaty is signed";
char *sig = NULL;
st = sign_message(msg, kp.private_key, alg, &sig);
if (!st.ok) {
fprintf(stderr, "%s sign: %s\n", alg_name, st.error);
sign_key_pair_free(&kp);
return;
}
printf(" signature: %.32s... (%zu hex chars)\n", sig, strlen(sig));
bool valid = false;
st = verify_signature(msg, sig, kp.public_key, alg, &valid);
printf(" verify original : %s\n", st.ok ? (valid ? "valid" : "INVALID") : st.error);
st = verify_signature("the treaty is void", sig, kp.public_key, alg, &valid);
printf(" verify tampered : %s\n", st.ok ? (valid ? "valid (bug!)" : "invalid")
: st.error);
free(sig);
sign_key_pair_free(&kp);
}
int main(void)
{
round_trip("ed25519");
round_trip("ecdsa-p256");
return 0;
}
#endif
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 →