Skip to content

Digital Signatures

Digital signatures use a key pair to provide authentication and integrity. The private key signs a message, producing a signature that anyone with the corresponding public key can verify. This proves both who produced the data and that it has not been tampered with.

Assumed imports

import dev.whyoleg.cryptography.*
import dev.whyoleg.cryptography.algorithms.*

val provider = CryptographyProvider.Default

Basic Usage

Generate a key pair, sign a message, and verify the signature using ECDSA with P-256:

val ecdsa = provider.get(ECDSA)
val keyPair = ecdsa.keyPairGenerator(EC.Curve.P256).generateKey()

// Sign a message with the private key
val message = "Transfer 100 EUR to Alice".encodeToByteArray()
val signature = keyPair.privateKey
    .signatureGenerator(digest = SHA256, format = ECDSA.SignatureFormat.DER)
    .generateSignature(message)

// Verify -- throws if the signature is invalid
keyPair.publicKey
    .signatureVerifier(digest = SHA256, format = ECDSA.SignatureFormat.DER)
    .verifySignature(message, signature)

There are two ways to verify the signature:

val verifier = keyPair.publicKey
    .signatureVerifier(digest = SHA256, format = ECDSA.SignatureFormat.DER)

// Option 1: throws on failure
verifier.verifySignature(message, signature)

// Option 2: Boolean result
val isValid = verifier.tryVerifySignature(message, signature)

Both signatureGenerator and signatureVerifier return reusable objects – create them once and call generateSignature or verifySignature as many times as needed.

For larger data, the overload that accepts a RawSource from kotlinx-io could be used instead:

val source: RawSource = ... // file, network stream, etc.
val signature = keyPair.privateKey
    .signatureGenerator(digest = SHA256, format = ECDSA.SignatureFormat.DER)
    .generateSignature(source)
keyPair.publicKey
    .signatureVerifier(digest = SHA256, format = ECDSA.SignatureFormat.DER)
    .verifySignature(source, signature)

Pass-Through

Use updatingSource or updatingSink to compute a signature as data flows through a kotlinx-io pipeline:

val signFunction = keyPair.privateKey
    .signatureGenerator(SHA256, ECDSA.SignatureFormat.DER)
    .createSignFunction()

// Wrap a source -- data passes through AND is fed into the signature
val signingSource: RawSource = signFunction.updatingSource(messageSource)

// Read all data (it goes to both the consumer and the signature function)
val data = signingSource.readByteArray()

// After reading, the signature is ready
val signature = signFunction.signToByteArray()

Both SignFunction and VerifyFunction implement AutoCloseable and support reset for a reuse. After finalization, call close, or wrap it in a use block:

val signature = keyPair.privateKey
    .signatureGenerator(SHA256, ECDSA.SignatureFormat.DER)
    .createSignFunction().use { sf ->
        // ... feed data to function
        sf.signToByteArray()
    }

For the most control, use update directly to feed data in arbitrary chunks:

val signFunction = keyPair.privateKey
    .signatureGenerator(SHA256, ECDSA.SignatureFormat.DER)
    .createSignFunction()

signFunction.update("first chunk ".encodeToByteArray())
signFunction.update("second chunk".encodeToByteArray())

val signature = signFunction.signToByteArray()

Incremental verification works the same way:

val verifyFunction = keyPair.publicKey
    .signatureVerifier(SHA256, ECDSA.SignatureFormat.DER)
    .createVerifyFunction()

verifyFunction.update("first chunk ".encodeToByteArray())
verifyFunction.update("second chunk".encodeToByteArray())

verifyFunction.verify(signature) // throws on failure

Supported Algorithms

Algorithm JDK WebCrypto Apple CryptoKit OpenSSL3
RSA-PSS ✅ 1 ✅ ✅ ❌ ✅
RSA-PKCS1 ✅ ✅ ✅ ❌ ✅
ECDSA ⭐ ✅ ✅ 2 ✅ ✅ 2 ✅
EdDSA ⭐ ✅ 3 ✅ 4 ❌ ✅ 5 ✅
DSA ✅ ❌ ❌ ❌ ✅

  1. Not available on Android; use BouncyCastle 

  2. Doesn’t support working with pre-hashed data 

  3. Requires JDK 15+; use BouncyCastle on older JDK or Android 

  4. Ed25519 only; browser support varies 

  5. Ed25519 only