Digital Signature — TypeScript source
Sign messages with Ed25519 or ECDSA and verify signatures. Prove authorship without revealing your private key.
This is the TypeScript implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Digital Signature - Ed25519 / ECDSA P-256 message signing and verification
// via the Web Crypto API. 100% client-side: keys, messages and signatures
// never leave the browser.
//
// 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.
export type SignAlgorithm = 'ed25519' | 'ecdsa-p256';
export interface KeyPair {
/** Hex-encoded SPKI (SubjectPublicKeyInfo) public key. Safe to share. */
publicKey: string;
/** Hex-encoded PKCS8 private key. Keep it secret - it IS the identity. */
privateKey: string;
}
const HEX = /^(?:[0-9a-fA-F]{2})+$/;
function subtle(): SubtleCrypto {
const c = globalThis.crypto;
if (!c?.subtle) {
throw new Error('Web Crypto is not available in this browser context.');
}
return c.subtle;
}
function assertAlgorithm(algorithm: SignAlgorithm): void {
if (algorithm !== 'ed25519' && algorithm !== 'ecdsa-p256') {
throw new Error('Unknown algorithm. Use "ed25519" or "ecdsa-p256".');
}
}
function label(algorithm: SignAlgorithm): string {
return algorithm === 'ed25519' ? 'Ed25519' : 'ECDSA P-256';
}
/** KeyGen params object for the requested algorithm (Web Crypto shape). */
function keyGenParams(algorithm: SignAlgorithm): Algorithm {
return algorithm === 'ed25519'
? { name: 'Ed25519' }
: { name: 'ECDSA', namedCurve: 'P-256' };
}
/** Sign/verify params object (ECDSA must pin the hash it signs with). */
function signParams(algorithm: SignAlgorithm): Algorithm {
return algorithm === 'ed25519' ? { name: 'Ed25519' } : { name: 'ECDSA', hash: 'SHA-256' };
}
function hexToBytes(hex: string, what: string): Uint8Array {
if (!hex || !HEX.test(hex)) {
throw new Error(`${what} must be a non-empty hex string (pairs of 0-9 / a-f digits).`);
}
const out = new Uint8Array(hex.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
}
return out;
}
function toHex(bytes: Uint8Array): string {
return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
}
/** Generate a fresh signing key pair, exported as hex SPKI / PKCS8 keys. */
export async function generateKeyPair(algorithm: SignAlgorithm): Promise<KeyPair> {
assertAlgorithm(algorithm);
const s = subtle(); // throws its own descriptive error when unavailable
let pair: CryptoKeyPair;
try {
pair = (await s.generateKey(keyGenParams(algorithm), true, ['sign', 'verify'])) as CryptoKeyPair;
} catch {
throw new Error(
algorithm === 'ed25519'
? 'Ed25519 is not supported by this browser. Switch to ECDSA P-256 or update your browser.'
: 'ECDSA P-256 key generation failed.'
);
}
const spki = new Uint8Array(await subtle().exportKey('spki', pair.publicKey));
const pkcs8 = new Uint8Array(await subtle().exportKey('pkcs8', pair.privateKey));
return { publicKey: toHex(spki), privateKey: toHex(pkcs8) };
}
async function importPrivateKey(privateKeyHex: string, algorithm: SignAlgorithm): Promise<CryptoKey> {
const bytes = hexToBytes(privateKeyHex, 'Private key');
try {
return await subtle().importKey('pkcs8', bytes, keyGenParams(algorithm), false, ['sign']);
} catch {
throw new Error(`Invalid private key for ${label(algorithm)} (expected hex PKCS8).`);
}
}
async function importPublicKey(publicKeyHex: string, algorithm: SignAlgorithm): Promise<CryptoKey> {
const bytes = hexToBytes(publicKeyHex, 'Public key');
try {
return await subtle().importKey('spki', bytes, keyGenParams(algorithm), false, ['verify']);
} catch {
throw new Error(`Invalid public key for ${label(algorithm)} (expected hex SPKI).`);
}
}
/** Sign `message` with a hex PKCS8 private key. Returns the hex signature. */
export async function signMessage(
message: string,
privateKeyHex: string,
algorithm: SignAlgorithm
): Promise<string> {
assertAlgorithm(algorithm);
if (message === '') {
throw new Error('Message must not be empty.');
}
const key = await importPrivateKey(privateKeyHex, algorithm);
const signature = new Uint8Array(
await subtle().sign(signParams(algorithm), key, new TextEncoder().encode(message))
);
return toHex(signature);
}
/**
* Verify `signatureHex` against `message` with a hex SPKI public key.
* Returns false when the signature, message or key simply do not match;
* throws only for malformed input (non-hex / wrong key type).
*/
export async function verifySignature(
message: string,
signatureHex: string,
publicKeyHex: string,
algorithm: SignAlgorithm
): Promise<boolean> {
assertAlgorithm(algorithm);
const signature = hexToBytes(signatureHex, 'Signature');
const key = await importPublicKey(publicKeyHex, algorithm);
return subtle().verify(
signParams(algorithm),
key,
signature,
new TextEncoder().encode(message)
);
}
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 →