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 →