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 →