Skip to content

Digital Signature — Java source

Sign messages with Ed25519 or ECDSA and verify signatures. Prove authorship without revealing your private key.

This is the Java 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 Java Cryptography Architecture. 100% local: keys, messages and
// signatures never leave the process.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/digital-signature.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// 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 (X509EncodedKeySpec / PKCS8EncodedKeySpec are those same
// structures). 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). Java's ECDSA engine emits DER, so this port converts between
// the two layouts at the sign/verify boundary. ECDSA signs the SHA-256
// digest of the message.

import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.spec.NamedParameterSpec;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.regex.Pattern;

public final class DigitalSignature {

    /** The two supported algorithms ("ed25519" / "ecdsa-p256" in the TS reference). */
    public enum SignAlgorithm {
        ED25519("Ed25519"),
        ECDSA_P256("ECDSA P-256");

        final String label;

        SignAlgorithm(String label) {
            this.label = label;
        }

        /** Parse the wire token used by the TS reference; unknown values throw. */
        public static SignAlgorithm of(String algorithm) {
            if ("ed25519".equals(algorithm)) return ED25519;
            if ("ecdsa-p256".equals(algorithm)) return ECDSA_P256;
            throw new IllegalArgumentException("Unknown algorithm. Use \"ed25519\" or \"ecdsa-p256\".");
        }
    }

    /** A signing key pair, both halves hex-encoded. */
    public record KeyPairHex(
            /** Hex-encoded SPKI (SubjectPublicKeyInfo) public key. Safe to share. */
            String publicKey,
            /** Hex-encoded PKCS8 private key. Keep it secret - it IS the identity. */
            String privateKey) {
    }

    private static final Pattern HEX = Pattern.compile("(?:[0-9a-fA-F]{2})+");

    private DigitalSignature() {
    }

    private static String label(SignAlgorithm algorithm) {
        return algorithm.label;
    }

    /** JCA key-pair-generator/KeyFactory name for the requested algorithm. */
    private static String keyFactoryName(SignAlgorithm algorithm) {
        return algorithm == SignAlgorithm.ED25519 ? "Ed25519" : "EC";
    }

    /** JCA Signature name: Ed25519 signs the message itself; P-256 pins SHA-256. */
    private static String signatureName(SignAlgorithm algorithm) {
        return algorithm == SignAlgorithm.ED25519 ? "Ed25519" : "SHA256withECDSA";
    }

    private static byte[] hexToBytes(String hex, String what) {
        if (hex == null || hex.isEmpty() || !HEX.matcher(hex).matches()) {
            throw new IllegalArgumentException(
                    what + " must be a non-empty hex string (pairs of 0-9 / a-f digits).");
        }
        byte[] out = new byte[hex.length() / 2];
        for (int i = 0; i < out.length; i++) {
            out[i] = (byte) Integer.parseInt(hex.substring(i * 2, i * 2 + 2), 16);
        }
        return out;
    }

    private static String toHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte b : bytes) {
            sb.append(Character.forDigit((b >> 4) & 0xf, 16));
            sb.append(Character.forDigit(b & 0xf, 16));
        }
        return sb.toString();
    }

    /** Generate a fresh signing key pair, exported as hex SPKI / PKCS8 keys. */
    public static KeyPairHex generateKeyPair(SignAlgorithm algorithm) {
        try {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance(keyFactoryName(algorithm));
            if (algorithm == SignAlgorithm.ED25519) {
                kpg.initialize(NamedParameterSpec.ED25519);
            } else {
                kpg.initialize(new ECGenParameterSpec("secp256r1"));
            }
            KeyPair pair = kpg.generateKeyPair();
            return new KeyPairHex(toHex(pair.getPublic().getEncoded()), toHex(pair.getPrivate().getEncoded()));
        } catch (Exception e) {
            throw new IllegalArgumentException(algorithm == SignAlgorithm.ED25519
                    ? "Ed25519 is not supported by this Java runtime. Switch to ECDSA P-256 or update the JDK."
                    : "ECDSA P-256 key generation failed.", e);
        }
    }

    private static PrivateKey importPrivateKey(String privateKeyHex, SignAlgorithm algorithm) {
        byte[] bytes = hexToBytes(privateKeyHex, "Private key");
        try {
            return KeyFactory.getInstance(keyFactoryName(algorithm))
                    .generatePrivate(new PKCS8EncodedKeySpec(bytes));
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    "Invalid private key for " + label(algorithm) + " (expected hex PKCS8).", e);
        }
    }

    private static PublicKey importPublicKey(String publicKeyHex, SignAlgorithm algorithm) {
        byte[] bytes = hexToBytes(publicKeyHex, "Public key");
        try {
            return KeyFactory.getInstance(keyFactoryName(algorithm))
                    .generatePublic(new X509EncodedKeySpec(bytes));
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    "Invalid public key for " + label(algorithm) + " (expected hex SPKI).", e);
        }
    }

    // --- ECDSA signature layout: Java emits ASN.1 DER, the tool's contract is
    //     IEEE P1363 r||s (fixed-width, 32+32 bytes for P-256) ---

    /** Minimal unsigned big-endian bytes of a non-negative integer (no sign byte). */
    private static byte[] unsignedBytes(BigInteger n) {
        byte[] b = n.toByteArray(); // signed two's complement
        int strip = 0;
        while (strip < b.length - 1 && b[strip] == 0) strip++; // leading zero padding only
        return java.util.Arrays.copyOfRange(b, strip, b.length);
    }

    /** Left-pad to `length` bytes. */
    private static byte[] pad(byte[] b, int length) {
        byte[] out = new byte[length];
        System.arraycopy(b, 0, out, length - b.length, b.length);
        return out;
    }

    /** DER SEQUENCE(INTEGER r, INTEGER s) -> 64-byte r||s (P-256 coordinate width). */
    private static byte[] derToP1363(byte[] der) {
        try {
            int pos = 0;
            if (der[pos++] != 0x30) throw new IllegalArgumentException("not a SEQUENCE");
            int seqLen = der[pos++] & 0xff;
            if ((seqLen & 0x80) != 0 || 2 + seqLen != der.length) {
                throw new IllegalArgumentException("bad SEQUENCE length");
            }
            BigInteger[] ints = new BigInteger[2];
            for (int i = 0; i < 2; i++) {
                if (der[pos++] != 0x02) throw new IllegalArgumentException("not an INTEGER");
                int len = der[pos++] & 0xff;
                ints[i] = new BigInteger(1, der, pos, len);
                pos += len;
            }
            int coordWidth = 32; // P-256
            byte[] out = new byte[coordWidth * 2];
            System.arraycopy(pad(unsignedBytes(ints[0]), coordWidth), 0, out, 0, coordWidth);
            System.arraycopy(pad(unsignedBytes(ints[1]), coordWidth), 0, out, coordWidth, coordWidth);
            return out;
        } catch (Exception e) {
            throw new IllegalArgumentException("Malformed ECDSA signature.", e);
        }
    }

    /** 64-byte r||s (any coordinate width that is half the input) -> DER SEQUENCE. */
    private static byte[] p1363ToDer(byte[] raw) {
        int half = raw.length / 2;
        BigInteger r = new BigInteger(1, raw, 0, half);
        BigInteger s = new BigInteger(1, raw, half, half);
        byte[] rb = r.toByteArray(); // signed two's complement: high bit set adds a 0 byte
        byte[] sb = s.toByteArray();
        int body = 2 + rb.length + 2 + sb.length;
        byte[] der = new byte[2 + body];
        der[0] = 0x30;
        der[1] = (byte) body;
        der[2] = 0x02;
        der[3] = (byte) rb.length;
        System.arraycopy(rb, 0, der, 4, rb.length);
        der[4 + rb.length] = 0x02;
        der[5 + rb.length] = (byte) sb.length;
        System.arraycopy(sb, 0, der, 6 + rb.length, sb.length);
        return der;
    }

    /** Sign `message` with a hex PKCS8 private key. Returns the hex signature. */
    public static String signMessage(String message, String privateKeyHex, SignAlgorithm algorithm) {
        if (message == null || message.isEmpty()) {
            throw new IllegalArgumentException("Message must not be empty.");
        }
        PrivateKey key = importPrivateKey(privateKeyHex, algorithm);
        try {
            Signature sig = Signature.getInstance(signatureName(algorithm));
            sig.initSign(key, new SecureRandom());
            sig.update(message.getBytes(StandardCharsets.UTF_8));
            byte[] signature = sig.sign();
            if (algorithm == SignAlgorithm.ECDSA_P256) signature = derToP1363(signature);
            return toHex(signature);
        } catch (Exception e) {
            throw new IllegalStateException("Signing failed: " + e.getMessage(), e);
        }
    }

    /**
     * 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).
     */
    public static boolean verifySignature(String message, String signatureHex,
            String publicKeyHex, SignAlgorithm algorithm) {
        byte[] signature = hexToBytes(signatureHex, "Signature");
        PublicKey key = importPublicKey(publicKeyHex, algorithm);
        try {
            Signature sig = Signature.getInstance(signatureName(algorithm));
            sig.initVerify(key);
            sig.update(message.getBytes(StandardCharsets.UTF_8));
            if (algorithm == SignAlgorithm.ECDSA_P256) {
                return sig.verify(p1363ToDer(signature));
            }
            return sig.verify(signature);
        } catch (Exception e) {
            throw new IllegalStateException("Verification failed: " + e.getMessage(), e);
        }
    }
}

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 →