Skip to content

WireGuard Key Generator — C source

Generate Curve25519 key pairs for WireGuard VPN configuration. Derives the public key from a clamped private key with a pure-BigInt RFC 7748 Montgomery ladder, optionally generates a pre-shared key, and renders a ready-to-edit wg-quick config template. Everything runs 100% client-side - keys never leave your browser.

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

/*
 * wireguard-keygen — WireGuard key generation (Curve25519 / RFC 7748 X25519).
 *
 * Language: C (C11, standard library + OpenSSL 3.x libcrypto — C has no
 *           bignum or CSPRNG in its standard library; libcrypto is the
 *           de-facto native choice)
 * Source:   CosmoDev polyglot showcase port of the WireGuard Keygen tool,
 *           ported from src/lib/wireguard-keygen.ts (the canonical TypeScript
 *           implementation).
 * License:  display source — part of CosmoDev's polyglot tool pages.
 *
 * WireGuard uses Curve25519 for its key exchange:
 *   - a private key is 32 random bytes, clamped per the Curve25519 rules
 *     (key[0] &= 248; key[31] &= 127; key[31] |= 64)
 *   - the public key is that scalar multiplied by the curve's base point 9,
 *     computed with a Montgomery ladder over GF(2^255 - 19)
 *   - an optional pre-shared key is 32 random bytes, used as-is (no clamping)
 *
 * Every key is serialized as standard Base64 with padding — 44 characters for
 * 32 bytes — which is exactly the format WireGuard config files expect.
 *
 * The TypeScript reference reaches for BigInt; C reaches for OpenSSL BIGNUM.
 * The ladder below is a line-for-line analogue of the TS one so the two can be
 * read side by side.
 *
 * NOTE: the conditional swap mirrors the reference's branch for readability.
 * A production X25519 must swap in constant time (see OpenSSL's X25519(), or
 * EVP_PKEY_X25519, which this file deliberately does not use so that the
 * curve arithmetic stays visible).
 *
 * Build: cc -std=c11 wireguard-keygen.c -lcrypto
 */

#include <ctype.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/rand.h>

/* -------------------------------------------------------------------------
 * Public types and constants
 * ------------------------------------------------------------------------- */

/** Length of every WireGuard key, in bytes. */
#define WG_KEY_LENGTH 32

/** A 32-byte key in standard padded Base64 is always 44 characters. */
#define WG_KEY_B64_LENGTH 44

typedef struct {
    char private_key[WG_KEY_B64_LENGTH + 1];
    char public_key[WG_KEY_B64_LENGTH + 1];
} wg_keys;

typedef enum {
    WG_OK = 0,
    WG_ERR_LENGTH,       /* wrong key/scalar length */
    WG_ERR_BASE64,       /* malformed Base64 input */
    WG_ERR_BUFFER,       /* caller buffer too small */
    WG_ERR_RANDOM,       /* CSPRNG failure */
    WG_ERR_INTERNAL      /* bignum allocation/arithmetic failure */
} wg_status;

/** Human-readable form of a wg_status, for error reporting. */
const char *wg_strerror(wg_status status)
{
    switch (status) {
    case WG_OK:            return "ok";
    case WG_ERR_LENGTH:    return "key must be 32 bytes";
    case WG_ERR_BASE64:    return "invalid Base64 input";
    case WG_ERR_BUFFER:    return "output buffer too small";
    case WG_ERR_RANDOM:    return "CSPRNG failure";
    case WG_ERR_INTERNAL:  return "internal bignum failure";
    default:               return "unknown error";
    }
}

static const char WG_B64_ALPHABET[] =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

/* u = 9, little-endian — the Curve25519 base point. */
static const uint8_t WG_BASE_POINT[WG_KEY_LENGTH] = { 9 };

/* -------------------------------------------------------------------------
 * Base64 codec (standard alphabet, always padded — the WireGuard format)
 * ------------------------------------------------------------------------- */

/**
 * Encode `len` bytes as standard Base64 with `=` padding (32 bytes -> 44
 * chars). `out_cap` must hold 4*ceil(len/3) + 1 bytes.
 */
wg_status wg_bytes_to_base64(const uint8_t *bytes, size_t len,
                             char *out, size_t out_cap)
{
    size_t needed = ((len + 2) / 3) * 4 + 1;
    size_t o = 0;

    if (bytes == NULL || out == NULL) return WG_ERR_BUFFER;
    if (out_cap < needed) return WG_ERR_BUFFER;

    for (size_t i = 0; i < len; i += 3) {
        uint8_t b0 = bytes[i];
        uint8_t b1 = (i + 1 < len) ? bytes[i + 1] : 0;
        uint8_t b2 = (i + 2 < len) ? bytes[i + 2] : 0;

        out[o++] = WG_B64_ALPHABET[b0 >> 2];
        out[o++] = WG_B64_ALPHABET[((b0 & 0x03) << 4) | (b1 >> 4)];
        out[o++] = (i + 1 < len)
                       ? WG_B64_ALPHABET[((b1 & 0x0f) << 2) | (b2 >> 6)]
                       : '=';
        out[o++] = (i + 2 < len) ? WG_B64_ALPHABET[b2 & 0x3f] : '=';
    }
    out[o] = '\0';
    return WG_OK;
}

/** Index of `ch` in the standard alphabet, or -1. Mirrors String.indexOf. */
static int wg_b64_index(char ch)
{
    const char *p = memchr(WG_B64_ALPHABET, ch, sizeof(WG_B64_ALPHABET) - 1);
    return (p == NULL) ? -1 : (int)(p - WG_B64_ALPHABET);
}

/**
 * Decode standard Base64 (with padding) into `out`. Leading/trailing
 * whitespace is trimmed first, exactly like the reference's String.trim().
 * Writes the decoded length to `*out_len`.
 */
wg_status wg_base64_to_bytes(const char *b64, uint8_t *out, size_t out_cap,
                             size_t *out_len)
{
    const char *start, *end;
    size_t len, pad, data_len, decoded, o = 0;
    uint32_t buffer = 0;
    int bits = 0;

    if (b64 == NULL || out == NULL || out_len == NULL) return WG_ERR_BUFFER;

    start = b64;
    while (*start != '\0' && isspace((unsigned char)*start)) start++;
    end = start + strlen(start);
    while (end > start && isspace((unsigned char)end[-1])) end--;
    len = (size_t)(end - start);

    if (len == 0 || len % 4 != 0) return WG_ERR_BASE64;

    if (len >= 2 && start[len - 1] == '=' && start[len - 2] == '=') pad = 2;
    else if (start[len - 1] == '=') pad = 1;
    else pad = 0;

    data_len = len - pad;
    decoded = (len / 4) * 3 - pad;
    if (out_cap < decoded) return WG_ERR_BUFFER;

    for (size_t i = 0; i < data_len; i++) {
        int v = wg_b64_index(start[i]);
        if (v < 0) return WG_ERR_BASE64;
        buffer = (buffer << 6) | (uint32_t)v;
        bits += 6;
        if (bits >= 8) {
            bits -= 8;
            out[o++] = (uint8_t)((buffer >> bits) & 0xff);
        }
    }

    *out_len = o;
    return WG_OK;
}

/* -------------------------------------------------------------------------
 * Curve25519 scalar multiplication (RFC 7748 Montgomery ladder)
 * ------------------------------------------------------------------------- */

/**
 * Clamp 32 bytes into a valid Curve25519 scalar (RFC 7748 section 5).
 * `out` may alias `key`.
 */
wg_status wg_clamp_private_key(const uint8_t *key, size_t len,
                               uint8_t out[WG_KEY_LENGTH])
{
    if (key == NULL || out == NULL) return WG_ERR_BUFFER;
    if (len != WG_KEY_LENGTH) return WG_ERR_LENGTH;

    memmove(out, key, WG_KEY_LENGTH);
    out[0] &= 248;   /* clear the low 3 bits -> multiple of the cofactor */
    out[31] &= 127;  /* clear the high bit */
    out[31] |= 64;   /* force bit 254 -> the ladder sees a 255-bit scalar */
    return WG_OK;
}

/** Little-endian bytes -> BIGNUM. */
static BIGNUM *wg_decode_le(const uint8_t *bytes, size_t len)
{
    uint8_t be[WG_KEY_LENGTH];

    if (len > sizeof(be)) return NULL;
    for (size_t i = 0; i < len; i++) be[i] = bytes[len - 1 - i];
    return BN_bin2bn(be, (int)len, NULL);
}

/** BIGNUM -> fixed-width little-endian bytes (zero-padded / truncated). */
static void wg_encode_le(const BIGNUM *n, uint8_t *out, size_t len)
{
    uint8_t be[WG_KEY_LENGTH] = { 0 };

    memset(out, 0, len);
    if (BN_bn2binpad(n, be, (int)len) < 0) return;
    for (size_t i = 0; i < len; i++) out[i] = be[len - 1 - i];
}

/** The field modulus 2^255 - 19. Caller frees. */
static BIGNUM *wg_field_prime(void)
{
    BIGNUM *p = BN_new();

    if (p == NULL) return NULL;
    if (!BN_set_word(p, 1) || !BN_lshift(p, p, 255) || !BN_sub_word(p, 19)) {
        BN_free(p);
        return NULL;
    }
    return p;
}

/**
 * X25519 scalar multiplication `scalar * u` — the RFC 7748 Montgomery ladder.
 * The scalar is clamped internally, exactly like every X25519 implementation.
 */
wg_status wg_curve25519(const uint8_t *scalar, size_t scalar_len,
                        const uint8_t *u, size_t u_len,
                        uint8_t out[WG_KEY_LENGTH])
{
    uint8_t clamped[WG_KEY_LENGTH];
    BN_CTX *ctx = NULL;
    BIGNUM *p = NULL, *a24 = NULL, *k = NULL, *x1 = NULL, *mask = NULL;
    BIGNUM *x2 = NULL, *z2 = NULL, *x3 = NULL, *z3 = NULL;
    BIGNUM *a = NULL, *aa = NULL, *b = NULL, *bb = NULL, *e = NULL;
    BIGNUM *c = NULL, *d = NULL, *da = NULL, *cb = NULL;
    BIGNUM *sum = NULL, *diff = NULL, *t = NULL, *exponent = NULL;
    int swap = 0;
    wg_status status = WG_ERR_INTERNAL;

    if (scalar == NULL || u == NULL || out == NULL) return WG_ERR_BUFFER;
    if (scalar_len != WG_KEY_LENGTH || u_len != WG_KEY_LENGTH)
        return WG_ERR_LENGTH;

    if (wg_clamp_private_key(scalar, scalar_len, clamped) != WG_OK)
        return WG_ERR_LENGTH;

    ctx = BN_CTX_new();
    if (ctx == NULL) return WG_ERR_INTERNAL;
    BN_CTX_start(ctx);

    p = wg_field_prime();
    k = wg_decode_le(clamped, WG_KEY_LENGTH);
    x1 = wg_decode_le(u, WG_KEY_LENGTH);
    mask = BN_new();
    a24 = BN_new();
    exponent = BN_new();
    if (p == NULL || k == NULL || x1 == NULL || mask == NULL || a24 == NULL ||
        exponent == NULL)
        goto done;

    /* (486662 - 2) / 4 */
    if (!BN_set_word(a24, 121665)) goto done;

    /* Mask the most significant bit of the u-coordinate (RFC 7748 section 5). */
    if (!BN_set_word(mask, 1) || !BN_lshift(mask, mask, 255) ||
        !BN_sub_word(mask, 1))
        goto done;
    if (!BN_mask_bits(x1, 255)) goto done;

    x2 = BN_CTX_get(ctx); z2 = BN_CTX_get(ctx);
    x3 = BN_CTX_get(ctx); z3 = BN_CTX_get(ctx);
    a  = BN_CTX_get(ctx); aa = BN_CTX_get(ctx);
    b  = BN_CTX_get(ctx); bb = BN_CTX_get(ctx);
    e  = BN_CTX_get(ctx); c  = BN_CTX_get(ctx);
    d  = BN_CTX_get(ctx); da = BN_CTX_get(ctx);
    cb = BN_CTX_get(ctx); sum = BN_CTX_get(ctx);
    diff = BN_CTX_get(ctx); t = BN_CTX_get(ctx);
    if (t == NULL) goto done;

    BN_zero(z2); /* BN_zero returns void in OpenSSL 3.x */
    if (!BN_one(x2) || !BN_copy(x3, x1) || !BN_one(z3))
        goto done;

    for (int bit_index = 254; bit_index >= 0; bit_index--) {
        int bit = BN_is_bit_set(k, bit_index);

        swap ^= bit;
        if (swap == 1) {
            BN_swap(x2, x3);
            BN_swap(z2, z3);
        }
        swap = bit;

        if (!BN_mod_add(a, x2, z2, p, ctx)) goto done;
        if (!BN_mod_sqr(aa, a, p, ctx)) goto done;
        if (!BN_mod_sub(b, x2, z2, p, ctx)) goto done;
        if (!BN_mod_sqr(bb, b, p, ctx)) goto done;
        if (!BN_mod_sub(e, aa, bb, p, ctx)) goto done;
        if (!BN_mod_add(c, x3, z3, p, ctx)) goto done;
        if (!BN_mod_sub(d, x3, z3, p, ctx)) goto done;
        if (!BN_mod_mul(da, d, a, p, ctx)) goto done;
        if (!BN_mod_mul(cb, c, b, p, ctx)) goto done;
        if (!BN_mod_add(sum, da, cb, p, ctx)) goto done;
        if (!BN_mod_sub(diff, da, cb, p, ctx)) goto done;

        if (!BN_mod_sqr(x3, sum, p, ctx)) goto done;
        if (!BN_mod_sqr(t, diff, p, ctx)) goto done;
        if (!BN_mod_mul(z3, x1, t, p, ctx)) goto done;
        if (!BN_mod_mul(x2, aa, bb, p, ctx)) goto done;
        if (!BN_mod_mul(t, a24, e, p, ctx)) goto done;
        if (!BN_mod_add(t, aa, t, p, ctx)) goto done;
        if (!BN_mod_mul(z2, e, t, p, ctx)) goto done;
    }

    /*
     * No final cswap: the loop leaves swap = k_0, and clamping clears bit 0,
     * so swap is provably 0 here for every input this function accepts.
     * x2 / z2 via z2^(p-2) (Fermat) gives the affine u-coordinate.
     */
    if (!BN_copy(exponent, p) || !BN_sub_word(exponent, 2)) goto done;
    if (!BN_mod_exp(t, z2, exponent, p, ctx)) goto done;
    if (!BN_mod_mul(t, x2, t, p, ctx)) goto done;

    wg_encode_le(t, out, WG_KEY_LENGTH);
    status = WG_OK;

done:
    BN_free(p);
    BN_free(k);
    BN_free(x1);
    BN_free(mask);
    BN_free(a24);
    BN_free(exponent);
    BN_CTX_end(ctx);
    BN_CTX_free(ctx);
    return status;
}

/* -------------------------------------------------------------------------
 * Key generation
 * ------------------------------------------------------------------------- */

static wg_status wg_random_bytes(uint8_t *out, size_t len)
{
    return (RAND_bytes(out, (int)len) == 1) ? WG_OK : WG_ERR_RANDOM;
}

/** A fresh private key: 32 CSPRNG bytes, clamped, Base64. */
wg_status wg_generate_private_key(char out[WG_KEY_B64_LENGTH + 1])
{
    uint8_t raw[WG_KEY_LENGTH];
    wg_status status = wg_random_bytes(raw, sizeof(raw));

    if (status != WG_OK) return status;
    status = wg_clamp_private_key(raw, sizeof(raw), raw);
    if (status != WG_OK) return status;
    return wg_bytes_to_base64(raw, sizeof(raw), out, WG_KEY_B64_LENGTH + 1);
}

/** A fresh pre-shared key: 32 CSPRNG bytes, Base64 — used as-is, never clamped. */
wg_status wg_generate_preshared_key(char out[WG_KEY_B64_LENGTH + 1])
{
    uint8_t raw[WG_KEY_LENGTH];
    wg_status status = wg_random_bytes(raw, sizeof(raw));

    if (status != WG_OK) return status;
    return wg_bytes_to_base64(raw, sizeof(raw), out, WG_KEY_B64_LENGTH + 1);
}

/**
 * Derive the WireGuard public key that pairs with a Base64 private key
 * (Curve25519 scalar multiplication of the base point).
 */
wg_status wg_private_key_to_public(const char *private_key_base64,
                                   char out[WG_KEY_B64_LENGTH + 1])
{
    uint8_t priv[WG_KEY_LENGTH];
    uint8_t pub[WG_KEY_LENGTH];
    size_t priv_len = 0;
    wg_status status;

    status = wg_base64_to_bytes(private_key_base64, priv, sizeof(priv), &priv_len);
    if (status != WG_OK) return status;
    if (priv_len != WG_KEY_LENGTH) return WG_ERR_LENGTH;

    status = wg_curve25519(priv, priv_len, WG_BASE_POINT, WG_KEY_LENGTH, pub);
    if (status != WG_OK) return status;

    return wg_bytes_to_base64(pub, sizeof(pub), out, WG_KEY_B64_LENGTH + 1);
}

/** A fresh WireGuard key pair (private + matching public key, both Base64). */
wg_status wg_generate_keys(wg_keys *keys)
{
    wg_status status;

    if (keys == NULL) return WG_ERR_BUFFER;
    status = wg_generate_private_key(keys->private_key);
    if (status != WG_OK) return status;
    return wg_private_key_to_public(keys->private_key, keys->public_key);
}

/* -------------------------------------------------------------------------
 * Config template
 * ------------------------------------------------------------------------- */

/**
 * Render a `wg-quick` config template around a key pair. The peer's public
 * key, endpoint, and your tunnel address depend on the other side, so they
 * stay as placeholders. A `PresharedKey` line is included only when `psk` is
 * a non-empty string (it must be present on BOTH sides of the tunnel).
 *
 * Returns a heap-allocated string the caller frees, or NULL on failure.
 */
char *wg_format_config(const wg_keys *keys, const char *psk)
{
    const char *header =
        "[Interface]\n"
        "# Your side — keep PrivateKey secret, share PublicKey with the peer\n";
    const char *middle =
        "# Tunnel address assigned by your server (plus optional tunnel DNS)\n"
        "Address = 10.0.0.2/32\n"
        "# DNS = 1.1.1.1\n"
        "\n"
        "[Peer]\n"
        "# The other side's public key\n"
        "PublicKey = <PEER_PUBLIC_KEY>\n";
    const char *footer =
        "# Route everything through the tunnel (or scope it, e.g. 10.0.0.0/24)\n"
        "AllowedIPs = 0.0.0.0/0, ::/0\n"
        "# The peer's public address and port\n"
        "Endpoint = vpn.example.com:51820\n"
        "PersistentKeepalive = 25";
    bool with_psk = (psk != NULL && psk[0] != '\0');
    size_t cap;
    char *out;
    int written;

    if (keys == NULL) return NULL;

    cap = strlen(header) + strlen(middle) + strlen(footer) + 512;
    if (with_psk) cap += strlen(psk);

    out = malloc(cap);
    if (out == NULL) return NULL;

    if (with_psk) {
        written = snprintf(out, cap,
                           "%s"
                           "PrivateKey = %s\n"
                           "PublicKey = %s\n"
                           "%s"
                           "# Optional pre-shared key — the same value must be set on BOTH sides\n"
                           "PresharedKey = %s\n"
                           "%s",
                           header, keys->private_key, keys->public_key,
                           middle, psk, footer);
    } else {
        written = snprintf(out, cap,
                           "%s"
                           "PrivateKey = %s\n"
                           "PublicKey = %s\n"
                           "%s"
                           "%s",
                           header, keys->private_key, keys->public_key,
                           middle, footer);
    }

    if (written < 0 || (size_t)written >= cap) {
        free(out);
        return NULL;
    }
    return out;
}

/* -------------------------------------------------------------------------
 * Demo
 * ------------------------------------------------------------------------- */

int main(void)
{
    wg_keys keys;
    char psk[WG_KEY_B64_LENGTH + 1];
    char *config;
    wg_status status;

    status = wg_generate_keys(&keys);
    if (status != WG_OK) {
        fprintf(stderr, "keygen failed: %s\n", wg_strerror(status));
        return 1;
    }

    status = wg_generate_preshared_key(psk);
    if (status != WG_OK) {
        fprintf(stderr, "psk failed: %s\n", wg_strerror(status));
        return 1;
    }

    config = wg_format_config(&keys, psk);
    if (config == NULL) {
        fprintf(stderr, "config rendering failed\n");
        return 1;
    }

    printf("%s\n", config);
    free(config);
    return 0;
}

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 →