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.
 *
 * 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 →