Digital Signature — Kotlin source
Sign messages with Ed25519 or ECDSA and verify signatures. Prove authorship without revealing your private key.
This is the Kotlin 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 JVM's java.security. 100% local: keys, messages and signatures
// never leave the process.
//
// Language: Kotlin 1.9+ (JVM 15+ for Ed25519), standard library only.
// Ported from src/lib/digital-signature.ts — display source, part of CosmoDev's
// polyglot tool pages. Functionally equivalent to the TS reference (which
// drives the browser's Web Crypto): same key encodings, same signature
// layout, interoperable byte for byte.
//
// 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.
//
// The JVM's Signature.getInstance("ECDSA") emits and consumes the ASN.1 DER
// form, so this port converts DER <-> P1363 on the way in and out - that is
// the only place the two runtimes differ.
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.Signature
import java.security.spec.ECGenParameterSpec
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
enum class SignAlgorithm { ED25519, ECDSA_P256 }
data class KeyPair(
/** Hex-encoded SPKI (SubjectPublicKeyInfo) public key. Safe to share. */
val publicKey: String,
/** Hex-encoded PKCS8 private key. Keep it secret - it IS the identity. */
val privateKey: String,
)
private val HEX = Regex("^(?:[0-9a-fA-F]{2})+$")
private fun label(algorithm: SignAlgorithm): String =
if (algorithm == SignAlgorithm.ED25519) "Ed25519" else "ECDSA P-256"
/** KeyGen algorithm name for the requested algorithm. */
private fun keyGenAlgorithm(algorithm: SignAlgorithm): String =
if (algorithm == SignAlgorithm.ED25519) "Ed25519" else "ECDSA"
/** Sign/verify transformation (ECDSA must pin the hash it signs with). */
private fun signTransformation(algorithm: SignAlgorithm): String =
if (algorithm == SignAlgorithm.ED25519) "Ed25519" else "SHA256withECDSA"
private fun hexToBytes(hex: String, what: String): ByteArray {
if (hex.isEmpty() || !HEX.matches(hex)) {
throw IllegalArgumentException("$what must be a non-empty hex string (pairs of 0-9 / a-f digits).")
}
return ByteArray(hex.length / 2) { i -> hex.substring(i * 2, i * 2 + 2).toInt(16).toByte() }
}
private fun toHex(bytes: ByteArray): String =
bytes.joinToString("") { "%02x".format(it) }
// ---------------------------------------------------------------------------
// ECDSA signature layout conversion: ASN.1 DER (SEQUENCE of two INTEGERs)
// <-> IEEE P1363 (fixed-width 32-byte r || 32-byte s).
// ---------------------------------------------------------------------------
/** DER INTEGER payload -> left-padded fixed-width big-endian. */
private fun derIntToFixed(payload: ByteArray, width: Int): ByteArray {
// payload is minimal big-endian, possibly shorter than width.
val out = ByteArray(width)
val take = minOf(payload.size, width)
System.arraycopy(payload, payload.size - take, out, width - take, take)
return out
}
/** Convert a DER-encoded ECDSA signature into the 64-byte P1363 r||s form. */
private fun derToP1363(der: ByteArray): ByteArray {
var i = 0
fun readLength(): Int {
val first = der[i++].toInt() and 0xFF
return if (first and 0x80 == 0) first else {
val n = first and 0x7F
var len = 0
repeat(n) { len = (len shl 8) or (der[i++].toInt() and 0xFF) }
len
}
}
require(der[i++].toInt() == 0x30) { "Not a DER SEQUENCE" } // SEQUENCE
readLength() // total length (unused)
require(der[i++].toInt() == 0x02) { "Expected DER INTEGER r" }
val rLen = readLength()
val r = derIntToFixed(der.copyOfRange(i, i + rLen), 32); i += rLen
require(der[i++].toInt() == 0x02) { "Expected DER INTEGER s" }
val sLen = readLength()
val s = derIntToFixed(der.copyOfRange(i, i + sLen), 32); i += sLen
return r + s
}
/** Convert a 64-byte P1363 r||s signature into the DER form the JVM wants. */
private fun p1363ToDer(raw: ByteArray): ByteArray {
fun trimLeadingZeros(b: ByteArray): ByteArray {
var start = 0
while (start < b.size - 1 && b[start].toInt() == 0) start++
val v = b.copyOfRange(start, b.size)
// DER INTEGERs are signed: prepend a zero byte when the high bit is set.
return if (v[0].toInt() and 0x80 != 0) byteArrayOf(0) + v else v
}
fun derInt(b: ByteArray): ByteArray {
val payload = trimLeadingZeros(b)
val len = if (payload.size < 0x80) byteArrayOf(payload.size.toByte())
else byteArrayOf(0x81.toByte(), payload.size.toByte())
return byteArrayOf(0x02) + len + payload
}
val r = derInt(raw.copyOfRange(0, 32))
val s = derInt(raw.copyOfRange(32, 64))
val body = r + s
val seqLen = if (body.size < 0x80) byteArrayOf(body.size.toByte())
else byteArrayOf(0x81.toByte(), body.size.toByte())
return byteArrayOf(0x30) + seqLen + body
}
// ---------------------------------------------------------------------------
// Public API (mirrors the TS reference one-for-one)
// ---------------------------------------------------------------------------
/** Generate a fresh signing key pair, exported as hex SPKI / PKCS8 keys. */
fun generateKeyPair(algorithm: SignAlgorithm): KeyPair {
val generator = KeyPairGenerator.getInstance(keyGenAlgorithm(algorithm))
if (algorithm == SignAlgorithm.ECDSA_P256) {
generator.initialize(ECGenParameterSpec("secp256r1"))
}
val pair = try {
generator.generateKeyPair()
} catch (e: Exception) {
throw IllegalArgumentException(
if (algorithm == SignAlgorithm.ED25519)
"Ed25519 is not supported by this JVM (needs Java 15+). Switch to ECDSA P-256 or update the runtime."
else "ECDSA P-256 key generation failed.",
e,
)
}
return KeyPair(toHex(pair.public.encoded), toHex(pair.private.encoded))
}
private fun importPrivateKey(privateKeyHex: String, algorithm: SignAlgorithm) =
try {
KeyFactory.getInstance(keyGenAlgorithm(algorithm))
.generatePrivate(PKCS8EncodedKeySpec(hexToBytes(privateKeyHex, "Private key")))
} catch (_: Exception) {
throw IllegalArgumentException("Invalid private key for ${label(algorithm)} (expected hex PKCS8).")
}
private fun importPublicKey(publicKeyHex: String, algorithm: SignAlgorithm) =
try {
KeyFactory.getInstance(keyGenAlgorithm(algorithm))
.generatePublic(X509EncodedKeySpec(hexToBytes(publicKeyHex, "Public key")))
} catch (_: Exception) {
throw IllegalArgumentException("Invalid public key for ${label(algorithm)} (expected hex SPKI).")
}
/** Sign `message` with a hex PKCS8 private key. Returns the hex signature. */
fun signMessage(message: String, privateKeyHex: String, algorithm: SignAlgorithm): String {
if (message.isEmpty()) {
throw IllegalArgumentException("Message must not be empty.")
}
val key = importPrivateKey(privateKeyHex, algorithm)
val signer = Signature.getInstance(signTransformation(algorithm))
signer.initSign(key)
signer.update(message.toByteArray(Charsets.UTF_8))
val raw = signer.sign()
return toHex(if (algorithm == SignAlgorithm.ED25519) raw else derToP1363(raw))
}
/**
* 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).
*/
fun verifySignature(message: String, signatureHex: String, publicKeyHex: String, algorithm: SignAlgorithm): Boolean {
val signature = hexToBytes(signatureHex, "Signature")
val key = importPublicKey(publicKeyHex, algorithm)
val verifier = Signature.getInstance(signTransformation(algorithm))
verifier.initVerify(key)
verifier.update(message.toByteArray(Charsets.UTF_8))
return if (algorithm == SignAlgorithm.ED25519) {
verifier.verify(signature)
} else {
verifier.verify(p1363ToDer(signature))
}
}
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 →