Digital Signature — Zig source
Sign messages with Ed25519 or ECDSA and verify signatures. Prove authorship without revealing your private key.
This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.
//! digital-signature — Ed25519 / ECDSA P-256 message signing and verification.
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/digital-signature.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! The TS reference drives the Web Crypto API; Zig's standard library ships
//! both algorithms as `std.crypto.sign.Ed25519` and
//! `std.crypto.sign.ecdsa.EcdsaP256Sha256`, so this port runs the whole
//! pipeline — key generation, signing, verification — locally with zero
//! dependencies. Keys and messages never leave the process.
//!
//! 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). ECDSA signs the SHA-256 digest of the message internally.
const std = @import("std");
pub const SignAlgorithm = enum {
ed25519,
ecdsa_p256,
/// Human label used in error messages ("Ed25519" / "ECDSA P-256").
pub fn label(self: SignAlgorithm) []const u8 {
return switch (self) {
.ed25519 => "Ed25519",
.ecdsa_p256 => "ECDSA P-256",
};
}
};
pub const KeyPair = struct {
/// Hex-encoded SPKI (SubjectPublicKeyInfo) public key. Safe to share.
public_key: []const u8,
/// Hex-encoded PKCS8 private key. Keep it secret - it IS the identity.
private_key: []const u8,
};
pub const Error = error{
EmptyMessage,
InvalidHex,
InvalidPrivateKey,
InvalidPublicKey,
InvalidSignature,
OutOfMemory,
};
const Ed25519 = std.crypto.sign.Ed25519;
const EcdsaP256Sha256 = std.crypto.sign.ecdsa.EcdsaP256Sha256;
// --- Hex codec (lowercase, matching the TS toHex) ---------------------------
fn toHex(allocator: std.mem.Allocator, bytes: []const u8) Error![]u8 {
const out = try allocator.alloc(u8, bytes.len * 2);
const charset = "0123456789abcdef";
for (bytes, 0..) |b, i| {
out[i * 2] = charset[b >> 4];
out[i * 2 + 1] = charset[b & 0x0f];
}
return out;
}
fn nibble(c: u8) Error!u8 {
return switch (c) {
'0'...'9' => c - '0',
'a'...'f' => c - 'a' + 10,
'A'...'F' => c - 'A' + 10,
else => Error.InvalidHex,
};
}
/// Decode a hex string into `out`. Fails on odd length or non-hex characters.
fn hexToBytes(out: []u8, hex: []const u8) Error!usize {
if (hex.len == 0 or hex.len % 2 != 0 or out.len * 2 < hex.len) return Error.InvalidHex;
var i: usize = 0;
while (i < hex.len) : (i += 2) {
const hi = try nibble(hex[i]);
const lo = try nibble(hex[i + 1]);
out[i / 2] = (hi << 4) | lo;
}
return hex.len / 2;
}
// --- Minimal DER builder (fixed SPKI / PKCS8 shapes) --------------------------
/// Append `content` wrapped as one TLV: `tag`, DER length, content bytes.
fn appendTlv(list: *std.ArrayList(u8), tag: u8, content: []const u8) !void {
try list.append(tag);
if (content.len < 0x80) {
try list.append(@intCast(content.len));
} else {
var len_bytes: [8]u8 = undefined;
var len = content.len;
var count: usize = 0;
while (len > 0) : (len >>= 8) {
len_bytes[count] = @intCast(len & 0xff);
count += 1;
}
try list.append(0x80 | @as(u8, @intCast(count)));
var j = count;
while (j > 0) {
j -= 1;
try list.append(len_bytes[j]);
}
}
try list.appendSlice(content);
}
/// DER OBJECT IDENTIFIER content bytes from a dotted string, e.g. "1.3.101.112".
fn appendOidContent(list: *std.ArrayList(u8), dotted: []const u8) !void {
var parts = std.mem.splitScalar(u8, dotted, '.');
const first = try std.fmt.parseInt(u32, parts.next().?, 10);
const second = try std.fmt.parseInt(u32, parts.next().?, 10);
try list.append(@intCast(40 * first + second));
while (parts.next()) |p| {
var v = try std.fmt.parseInt(u32, p, 10);
var stack: [8]u8 = undefined;
var depth: usize = 0;
while (true) {
stack[depth] = @intCast(v & 0x7f);
depth += 1;
v >>= 7;
if (v == 0) break;
}
var j = depth;
while (j > 1) {
j -= 1;
try list.append(stack[j] | 0x80);
}
try list.append(stack[0]);
}
}
// OID 1.3.101.112 = Ed25519, 1.2.840.10045.2.1 = ecPublicKey, 1.2.840.10045.3.1.7 = P-256.
const OID_ED25519 = "1.3.101.112";
const OID_EC_PUBLIC_KEY = "1.2.840.10045.2.1";
const OID_P256 = "1.2.840.10045.3.1.7";
/// SEQUENCE { OID } — the AlgorithmIdentifier for Ed25519 (no parameters).
fn ed25519AlgId(list: *std.ArrayList(u8)) !void {
var content = std.ArrayList(u8).init(list.allocator);
defer content.deinit();
try appendTlv(&content, 0x06, try oidSlice(list.allocator, OID_ED25519));
try appendTlv(list, 0x30, content.items);
}
/// SEQUENCE { OID ecPublicKey, OID prime256v1 } — the ECDSA AlgorithmIdentifier.
fn ecdsaAlgId(list: *std.ArrayList(u8)) !void {
var content = std.ArrayList(u8).init(list.allocator);
defer content.deinit();
try appendTlv(&content, 0x06, try oidSlice(list.allocator, OID_EC_PUBLIC_KEY));
try appendTlv(&content, 0x06, try oidSlice(list.allocator, OID_P256));
try appendTlv(list, 0x30, content.items);
}
/// The encoded content bytes of a dotted OID in a freshly allocated slice.
fn oidSlice(allocator: std.mem.Allocator, dotted: []const u8) ![]u8 {
var tmp = std.ArrayList(u8).init(allocator);
errdefer tmp.deinit();
try appendOidContent(&tmp, dotted);
return tmp.toOwnedSlice();
}
// --- SPKI / PKCS8 export (exactly the Web Crypto wire format) -----------------
/// Ed25519 SPKI: SEQUENCE { AlgorithmIdentifier, BIT STRING (0, A) } — 44 bytes.
fn ed25519Spki(allocator: std.mem.Allocator, public: *const [32]u8) ![]u8 {
var body = std.ArrayList(u8).init(allocator);
defer body.deinit();
try ed25519AlgId(&body);
var bits: [33]u8 = undefined;
bits[0] = 0; // 0 unused bits in the last octet
@memcpy(bits[1..], public);
var bits_tlv = std.ArrayList(u8).init(allocator);
defer bits_tlv.deinit();
try appendTlv(&bits_tlv, 0x03, &bits);
try body.appendSlice(bits_tlv.items);
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try appendTlv(&out, 0x30, body.items);
return out.toOwnedSlice();
}
/// Ed25519 PKCS8: SEQUENCE { INTEGER 0, AlgorithmIdentifier, OCTET STRING seed }.
fn ed25519Pkcs8(allocator: std.mem.Allocator, seed: *const [32]u8) ![]u8 {
var body = std.ArrayList(u8).init(allocator);
defer body.deinit();
try appendTlv(&body, 0x02, &[_]u8{0}); // version 0
try ed25519AlgId(&body);
var octet = std.ArrayList(u8).init(allocator);
defer octet.deinit();
try appendTlv(&octet, 0x04, seed);
try body.appendSlice(octet.items);
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try appendTlv(&out, 0x30, body.items);
return out.toOwnedSlice();
}
/// ECDSA P-256 SPKI: SEQUENCE { SEQUENCE { ecPublicKey, P-256 }, BIT STRING point }.
fn ecdsaSpki(allocator: std.mem.Allocator, sec1: *const [65]u8) ![]u8 {
var body = std.ArrayList(u8).init(allocator);
defer body.deinit();
try ecdsaAlgId(&body);
var bits: [66]u8 = undefined;
bits[0] = 0;
@memcpy(bits[1..], sec1);
var bits_tlv = std.ArrayList(u8).init(allocator);
defer bits_tlv.deinit();
try appendTlv(&bits_tlv, 0x03, &bits);
try body.appendSlice(bits_tlv.items);
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try appendTlv(&out, 0x30, body.items);
return out.toOwnedSlice();
}
/// ECDSA P-256 PKCS8: SEQUENCE { INTEGER 0, SEQUENCE { ecPublicKey, P-256 }, OCTET STRING scalar }.
fn ecdsaPkcs8(allocator: std.mem.Allocator, scalar: *const [32]u8) ![]u8 {
var body = std.ArrayList(u8).init(allocator);
defer body.deinit();
try appendTlv(&body, 0x02, &[_]u8{0});
try ecdsaAlgId(&body);
var octet = std.ArrayList(u8).init(allocator);
defer octet.deinit();
try appendTlv(&octet, 0x04, scalar);
try body.appendSlice(octet.items);
var out = std.ArrayList(u8).init(allocator);
errdefer out.deinit();
try appendTlv(&out, 0x30, body.items);
return out.toOwnedSlice();
}
// --- Key generation -----------------------------------------------------------
/// Generate a fresh signing key pair, exported as hex SPKI / PKCS8 keys.
/// Caller owns both hex strings.
pub fn generateKeyPair(allocator: std.mem.Allocator, algorithm: SignAlgorithm) Error!KeyPair {
switch (algorithm) {
.ed25519 => {
const kp = Ed25519.KeyPair.generate();
const public = kp.public_key.toBytes();
const seed = kp.secret_key.seed();
const spki = try ed25519Spki(allocator, &public);
defer allocator.free(spki);
const pkcs8 = try ed25519Pkcs8(allocator, &seed);
defer allocator.free(pkcs8);
return .{
.public_key = try toHex(allocator, spki),
.private_key = try toHex(allocator, pkcs8),
};
},
.ecdsa_p256 => {
const kp = try EcdsaP256Sha256.KeyPair.create();
const sec1 = kp.public_key.toSec1();
const sk = kp.secret_key.toBytes();
const spki = try ecdsaSpki(allocator, &sec1);
defer allocator.free(spki);
const pkcs8 = try ecdsaPkcs8(allocator, &sk);
defer allocator.free(pkcs8);
return .{
.public_key = try toHex(allocator, spki),
.private_key = try toHex(allocator, pkcs8),
};
},
}
}
// --- Signing / verification ---------------------------------------------------
/// Sign `message` with a hex PKCS8 private key. Returns the hex signature
/// (128 hex chars = 64 raw bytes for both algorithms). Caller owns it.
pub fn signMessage(allocator: std.mem.Allocator, message: []const u8, private_key_hex: []const u8, algorithm: SignAlgorithm) Error![]u8 {
if (message.len == 0) return Error.EmptyMessage;
var der: [256]u8 = undefined;
const der_len = try hexToBytes(&der, private_key_hex);
switch (algorithm) {
.ed25519 => {
// PKCS8 tail is the 32-byte seed — walk past the fixed header.
if (der_len < 48) return Error.InvalidPrivateKey;
const seed = der[der_len - 32 ..][0..32];
const kp = Ed25519.KeyPair.create(seed.*) catch return Error.InvalidPrivateKey;
const sig = Ed25519.sign(message, kp.secret_key, null);
return toHex(allocator, &sig.toBytes());
},
.ecdsa_p256 => {
if (der_len < 64) return Error.InvalidPrivateKey;
// PKCS8 tail is the 32-byte scalar (the private exponent).
const scalar = der[der_len - 32 ..][0..32];
const sk = EcdsaP256Sha256.SecretKey.fromBytes(scalar.*) catch return Error.InvalidPrivateKey;
const sig = try EcdsaP256Sha256.sign(message, sk);
return toHex(allocator, &sig.toRawBytes());
},
}
}
/// Verify `signature_hex` against `message` with a hex SPKI public key.
/// Returns false when the signature, message or key do not match; errors
/// only for malformed input (non-hex / wrong key type).
pub fn verifySignature(message: []const u8, signature_hex: []const u8, public_key_hex: []const u8, algorithm: SignAlgorithm) Error!bool {
var sig_bytes: [64]u8 = undefined;
const sig_len = try hexToBytes(&sig_bytes, signature_hex);
if (sig_len != 64) return Error.InvalidSignature;
var der: [512]u8 = undefined;
const der_len = try hexToBytes(&der, public_key_hex);
switch (algorithm) {
.ed25519 => {
// SPKI tail is the 32-byte compressed point.
if (der_len < 44) return Error.InvalidPublicKey;
const point = der[der_len - 32 ..][0..32];
const pk = Ed25519.PublicKey.fromBytes(point.*) catch return Error.InvalidPublicKey;
const sig = Ed25519.Signature.fromBytes(sig_bytes);
sig.verify(message, pk) catch return false;
return true;
},
.ecdsa_p256 => {
// SPKI tail is the 65-byte uncompressed point (0x04 || X || Y).
if (der_len < 91) return Error.InvalidPublicKey;
const sec1 = der[der_len - 65 ..][0..65];
const pk = EcdsaP256Sha256.PublicKey.fromSec1(sec1) catch return Error.InvalidPublicKey;
const sig = EcdsaP256Sha256.Signature.fromBytes(sig_bytes);
EcdsaP256Sha256.verify(message, sig, pk) catch return false;
return true;
},
}
}
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 →