From 7c1515c811dbabfb8706df97ea94c2ebc84f9da0 Mon Sep 17 00:00:00 2001 From: Aria Wisp Date: Sat, 6 Sep 2025 19:52:58 -0600 Subject: [PATCH] openssl3: implement EdDSA/XDH; key agreement refactor; nullable-hash signatures; wire provider; use Accumulating*; use ObjectIdentifier OIDs --- .../kotlin/Openssl3CryptographyProvider.kt | 2 + .../kotlin/algorithms/Openssl3EdDSA.kt | 143 ++++++++++++++++++ .../kotlin/algorithms/Openssl3XDH.kt | 131 ++++++++++++++++ .../kotlin/operations/KeyAgreement.kt | 31 ++++ .../Openssl3DigestSignatureGenerator.kt | 44 +++++- .../Openssl3DigestSignatureVerifier.kt | 47 +++++- 6 files changed, 388 insertions(+), 10 deletions(-) create mode 100644 cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3EdDSA.kt create mode 100644 cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3XDH.kt create mode 100644 cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/KeyAgreement.kt diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/Openssl3CryptographyProvider.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/Openssl3CryptographyProvider.kt index 426fb6b9..192cf3b8 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/Openssl3CryptographyProvider.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/Openssl3CryptographyProvider.kt @@ -38,6 +38,8 @@ internal object Openssl3CryptographyProvider : CryptographyProvider() { AES.GCM -> Openssl3AesGcm ECDSA -> Openssl3Ecdsa ECDH -> Openssl3Ecdh + EdDSA -> Openssl3EdDSA + XDH -> Openssl3XDH RSA.PSS -> Openssl3RsaPss RSA.PKCS1 -> Openssl3RsaPkcs1 RSA.OAEP -> Openssl3RsaOaep diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3EdDSA.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3EdDSA.kt new file mode 100644 index 00000000..1b0f83e2 --- /dev/null +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3EdDSA.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.openssl3.algorithms + +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.materials.key.* +import dev.whyoleg.cryptography.operations.* +import dev.whyoleg.cryptography.providers.base.* +import dev.whyoleg.cryptography.providers.openssl3.internal.* +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.* +import dev.whyoleg.cryptography.providers.openssl3.materials.* +import kotlinx.cinterop.* +import platform.posix.* +import kotlin.experimental.* +import dev.whyoleg.cryptography.serialization.asn1.modules.* +import dev.whyoleg.cryptography.serialization.asn1.ObjectIdentifier +import dev.whyoleg.cryptography.providers.base.materials.* +import dev.whyoleg.cryptography.providers.openssl3.operations.Openssl3DigestSignatureGenerator +import dev.whyoleg.cryptography.providers.openssl3.operations.Openssl3DigestSignatureVerifier + +internal object Openssl3EdDSA : EdDSA { + private fun algorithmName(curve: EdDSA.Curve): String = when (curve) { + EdDSA.Curve.Ed25519 -> "ED25519" + EdDSA.Curve.Ed448 -> "ED448" + } + private fun oid(curve: EdDSA.Curve): ObjectIdentifier = when (curve) { + EdDSA.Curve.Ed25519 -> ObjectIdentifier.Ed25519 + EdDSA.Curve.Ed448 -> ObjectIdentifier.Ed448 + } + + override fun publicKeyDecoder(curve: EdDSA.Curve): KeyDecoder = + object : Openssl3PublicKeyDecoder(algorithmName(curve)) { + override fun inputType(format: EdDSA.PublicKey.Format): String = when (format) { + EdDSA.PublicKey.Format.DER -> "DER" + EdDSA.PublicKey.Format.PEM -> "PEM" + EdDSA.PublicKey.Format.JWK, + EdDSA.PublicKey.Format.RAW -> error("should not be called: handled explicitly in decodeFromBlocking") + } + + override fun decodeFromByteArrayBlocking(format: EdDSA.PublicKey.Format, bytes: ByteArray): EdDSA.PublicKey = when (format) { + EdDSA.PublicKey.Format.RAW -> super.decodeFromByteArrayBlocking( + EdDSA.PublicKey.Format.DER, + wrapSubjectPublicKeyInfo(UnknownKeyAlgorithmIdentifier(oid(curve)), bytes) + ) + else -> super.decodeFromByteArrayBlocking(format, bytes) + } + + override fun wrapKey(key: CPointer): EdDSA.PublicKey = EdDsaPublicKey(key, curve) + } + + override fun privateKeyDecoder(curve: EdDSA.Curve): KeyDecoder = + object : Openssl3PrivateKeyDecoder(algorithmName(curve)) { + override fun inputType(format: EdDSA.PrivateKey.Format): String = when (format) { + EdDSA.PrivateKey.Format.DER -> "DER" + EdDSA.PrivateKey.Format.PEM -> "PEM" + EdDSA.PrivateKey.Format.JWK, + EdDSA.PrivateKey.Format.RAW -> error("should not be called: handled explicitly in decodeFromBlocking") + } + + override fun decodeFromByteArrayBlocking(format: EdDSA.PrivateKey.Format, bytes: ByteArray): EdDSA.PrivateKey = when (format) { + EdDSA.PrivateKey.Format.RAW -> super.decodeFromByteArrayBlocking( + EdDSA.PrivateKey.Format.DER, + wrapPrivateKeyInfo(0, UnknownKeyAlgorithmIdentifier(oid(curve)), bytes) + ) + else -> super.decodeFromByteArrayBlocking(format, bytes) + } + + override fun wrapKey(key: CPointer): EdDSA.PrivateKey = EdDsaPrivateKey(key, curve) + } + + override fun keyPairGenerator(curve: EdDSA.Curve): KeyGenerator = + object : Openssl3KeyPairGenerator(algorithmName(curve)) { + override fun MemScope.createParams(): CValuesRef? = null + override fun wrapKeyPair(keyPair: CPointer): EdDSA.KeyPair = EdDsaKeyPair( + publicKey = EdDsaPublicKey(keyPair, curve), + privateKey = EdDsaPrivateKey(keyPair, curve) + ) + } + + private class EdDsaKeyPair( + override val publicKey: EdDSA.PublicKey, + override val privateKey: EdDSA.PrivateKey, + ) : EdDSA.KeyPair + + private class EdDsaPublicKey( + key: CPointer, + private val curve: EdDSA.Curve, + ) : EdDSA.PublicKey, Openssl3PublicKeyEncodable(key) { + override fun outputType(format: EdDSA.PublicKey.Format): String = when (format) { + EdDSA.PublicKey.Format.DER -> "DER" + EdDSA.PublicKey.Format.PEM -> "PEM" + EdDSA.PublicKey.Format.JWK, + EdDSA.PublicKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") + } + + override fun encodeToByteArrayBlocking(format: EdDSA.PublicKey.Format): ByteArray = when (format) { + EdDSA.PublicKey.Format.RAW -> unwrapSubjectPublicKeyInfo( + oid(curve), + super.encodeToByteArrayBlocking(EdDSA.PublicKey.Format.DER) + ) + else -> super.encodeToByteArrayBlocking(format) + } + + override fun signatureVerifier(): SignatureVerifier = EdDsaSignatureVerifier(key) + } + + private class EdDsaPrivateKey( + key: CPointer, + private val curve: EdDSA.Curve, + ) : EdDSA.PrivateKey, Openssl3PrivateKeyEncodable(key) { + override fun outputType(format: EdDSA.PrivateKey.Format): String = when (format) { + EdDSA.PrivateKey.Format.DER -> "DER" + EdDSA.PrivateKey.Format.PEM -> "PEM" + EdDSA.PrivateKey.Format.JWK, + EdDSA.PrivateKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") + } + + override fun encodeToByteArrayBlocking(format: EdDSA.PrivateKey.Format): ByteArray = when (format) { + EdDSA.PrivateKey.Format.RAW -> unwrapPrivateKeyInfo( + oid(curve), + super.encodeToByteArrayBlocking(EdDSA.PrivateKey.Format.DER) + ) + else -> super.encodeToByteArrayBlocking(format) + } + + override fun signatureGenerator(): SignatureGenerator = EdDsaSignatureGenerator(key) + } +} + + +private class EdDsaSignatureGenerator( + private val privateKey: CPointer, +) : Openssl3DigestSignatureGenerator(privateKey, hashAlgorithm = null) { + override fun MemScope.createParams(): CValuesRef? = null +} + +private class EdDsaSignatureVerifier( + private val publicKey: CPointer, +) : Openssl3DigestSignatureVerifier(publicKey, hashAlgorithm = null) { + override fun MemScope.createParams(): CValuesRef? = null +} diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3XDH.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3XDH.kt new file mode 100644 index 00000000..905f6d1d --- /dev/null +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/algorithms/Openssl3XDH.kt @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.openssl3.algorithms + +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.materials.key.* +import dev.whyoleg.cryptography.operations.* +import dev.whyoleg.cryptography.providers.base.* +import dev.whyoleg.cryptography.providers.openssl3.internal.* +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.* +import dev.whyoleg.cryptography.providers.openssl3.materials.* +import kotlinx.cinterop.* +import platform.posix.* +import kotlin.experimental.* +import dev.whyoleg.cryptography.providers.openssl3.operations.* +import dev.whyoleg.cryptography.serialization.asn1.modules.* +import dev.whyoleg.cryptography.serialization.asn1.ObjectIdentifier +import dev.whyoleg.cryptography.providers.base.materials.* + +internal object Openssl3XDH : XDH { + private fun algorithmName(curve: XDH.Curve): String = when (curve) { + XDH.Curve.X25519 -> "X25519" + XDH.Curve.X448 -> "X448" + } + private fun oid(curve: XDH.Curve): ObjectIdentifier = when (curve) { + XDH.Curve.X25519 -> ObjectIdentifier.X25519 + XDH.Curve.X448 -> ObjectIdentifier.X448 + } + + override fun publicKeyDecoder(curve: XDH.Curve): KeyDecoder = + object : Openssl3PublicKeyDecoder(algorithmName(curve)) { + override fun inputType(format: XDH.PublicKey.Format): String = when (format) { + XDH.PublicKey.Format.DER -> "DER" + XDH.PublicKey.Format.PEM -> "PEM" + XDH.PublicKey.Format.JWK, + XDH.PublicKey.Format.RAW -> error("should not be called: handled explicitly in decodeFromBlocking") + } + + override fun decodeFromByteArrayBlocking(format: XDH.PublicKey.Format, bytes: ByteArray): XDH.PublicKey = when (format) { + XDH.PublicKey.Format.RAW -> super.decodeFromByteArrayBlocking( + XDH.PublicKey.Format.DER, + wrapSubjectPublicKeyInfo(UnknownKeyAlgorithmIdentifier(oid(curve)), bytes) + ) + else -> super.decodeFromByteArrayBlocking(format, bytes) + } + + override fun wrapKey(key: CPointer): XDH.PublicKey = XdhPublicKey(key, curve) + } + + override fun privateKeyDecoder(curve: XDH.Curve): KeyDecoder = + object : Openssl3PrivateKeyDecoder(algorithmName(curve)) { + override fun inputType(format: XDH.PrivateKey.Format): String = when (format) { + XDH.PrivateKey.Format.DER -> "DER" + XDH.PrivateKey.Format.PEM -> "PEM" + XDH.PrivateKey.Format.JWK, + XDH.PrivateKey.Format.RAW -> error("should not be called: handled explicitly in decodeFromBlocking") + } + + override fun decodeFromByteArrayBlocking(format: XDH.PrivateKey.Format, bytes: ByteArray): XDH.PrivateKey = when (format) { + XDH.PrivateKey.Format.RAW -> super.decodeFromByteArrayBlocking( + XDH.PrivateKey.Format.DER, + wrapPrivateKeyInfo(0, UnknownKeyAlgorithmIdentifier(oid(curve)), bytes) + ) + else -> super.decodeFromByteArrayBlocking(format, bytes) + } + + override fun wrapKey(key: CPointer): XDH.PrivateKey = XdhPrivateKey(key, curve) + } + + override fun keyPairGenerator(curve: XDH.Curve): KeyGenerator = + object : Openssl3KeyPairGenerator(algorithmName(curve)) { + override fun MemScope.createParams(): CValuesRef? = null + override fun wrapKeyPair(keyPair: CPointer): XDH.KeyPair = XdhKeyPair( + publicKey = XdhPublicKey(keyPair, curve), + privateKey = XdhPrivateKey(keyPair, curve) + ) + } + + private class XdhKeyPair( + override val publicKey: XDH.PublicKey, + override val privateKey: XDH.PrivateKey, + ) : XDH.KeyPair + + private class XdhPublicKey( + key: CPointer, + private val curve: XDH.Curve, + ) : XDH.PublicKey, Openssl3PublicKeyEncodable(key), SharedSecretGenerator { + override fun outputType(format: XDH.PublicKey.Format): String = when (format) { + XDH.PublicKey.Format.DER -> "DER" + XDH.PublicKey.Format.PEM -> "PEM" + XDH.PublicKey.Format.JWK, + XDH.PublicKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") + } + + override fun encodeToByteArrayBlocking(format: XDH.PublicKey.Format): ByteArray = when (format) { + XDH.PublicKey.Format.RAW -> unwrapSubjectPublicKeyInfo(oid(curve), super.encodeToByteArrayBlocking(XDH.PublicKey.Format.DER)) + else -> super.encodeToByteArrayBlocking(format) + } + + override fun sharedSecretGenerator(): SharedSecretGenerator = this + override fun generateSharedSecretToByteArrayBlocking(other: XDH.PrivateKey): ByteArray { + check(other is XdhPrivateKey) + return deriveSharedSecret(publicKey = key, privateKey = other.key) + } + } + + private class XdhPrivateKey( + key: CPointer, + private val curve: XDH.Curve, + ) : XDH.PrivateKey, Openssl3PrivateKeyEncodable(key), SharedSecretGenerator { + override fun outputType(format: XDH.PrivateKey.Format): String = when (format) { + XDH.PrivateKey.Format.DER -> "DER" + XDH.PrivateKey.Format.PEM -> "PEM" + XDH.PrivateKey.Format.JWK, + XDH.PrivateKey.Format.RAW -> error("should not be called: handled explicitly in encodeToBlocking") + } + + override fun encodeToByteArrayBlocking(format: XDH.PrivateKey.Format): ByteArray = when (format) { + XDH.PrivateKey.Format.RAW -> unwrapPrivateKeyInfo(oid(curve), super.encodeToByteArrayBlocking(XDH.PrivateKey.Format.DER)) + else -> super.encodeToByteArrayBlocking(format) + } + + override fun sharedSecretGenerator(): SharedSecretGenerator = this + override fun generateSharedSecretToByteArrayBlocking(other: XDH.PublicKey): ByteArray { + check(other is XdhPublicKey) + return deriveSharedSecret(publicKey = other.key, privateKey = key) + } + } +} diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/KeyAgreement.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/KeyAgreement.kt new file mode 100644 index 00000000..5ded6a9a --- /dev/null +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/KeyAgreement.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.whyoleg.cryptography.providers.openssl3.operations + +import dev.whyoleg.cryptography.providers.openssl3.internal.* +import dev.whyoleg.cryptography.providers.base.* +import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.* +import kotlinx.cinterop.* +import platform.posix.* +import kotlin.experimental.* + +@OptIn(UnsafeNumber::class) +internal fun deriveSharedSecret( + publicKey: CPointer, + privateKey: CPointer, +): ByteArray = memScoped { + val context = checkError(EVP_PKEY_CTX_new_from_pkey(null, privateKey, null)) + try { + checkError(EVP_PKEY_derive_init(context)) + checkError(EVP_PKEY_derive_set_peer(context, publicKey)) + val secretSize = alloc() + checkError(EVP_PKEY_derive(context, null, secretSize.ptr)) + val secret = ByteArray(secretSize.value.toInt()) + checkError(EVP_PKEY_derive(context, secret.refToU(0), secretSize.ptr)) + secret + } finally { + EVP_PKEY_CTX_free(context) + } +} diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3DigestSignatureGenerator.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3DigestSignatureGenerator.kt index 13336192..c65a57c5 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3DigestSignatureGenerator.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3DigestSignatureGenerator.kt @@ -8,25 +8,57 @@ import dev.whyoleg.cryptography.operations.* import dev.whyoleg.cryptography.providers.base.* import dev.whyoleg.cryptography.providers.openssl3.internal.* import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.* +import dev.whyoleg.cryptography.providers.base.operations.* import kotlinx.cinterop.* import platform.posix.* import kotlin.experimental.* internal abstract class Openssl3DigestSignatureGenerator( private val privateKey: CPointer, - private val hashAlgorithm: String, + // when null, performs one-shot signing (e.g., EdDSA) + private val hashAlgorithm: String?, ) : SignatureGenerator { @OptIn(ExperimentalNativeApi::class) private val cleaner = privateKey.upRef().cleaner() protected abstract fun MemScope.createParams(): CValuesRef? - override fun createSignFunction(): SignFunction { - return Openssl3DigestSignFunction(Resource(checkError(EVP_MD_CTX_new()), ::EVP_MD_CTX_free)) + override fun createSignFunction(): SignFunction = when (hashAlgorithm) { + null -> AccumulatingSignFunction { data -> + memScoped { + val ctx = checkError(EVP_MD_CTX_new()) + try { + checkError( + EVP_DigestSignInit_ex( + ctx = ctx, + pctx = null, + mdname = null, // one-shot mode + libctx = null, + props = null, + pkey = privateKey, + params = createParams() + ) + ) + val siglen = alloc() + data.usePinned { pin -> + checkError(EVP_DigestSign(ctx, null, siglen.ptr, pin.safeAddressOfU(0), data.size.convert())) + val out = ByteArray(siglen.value.convert()) + out.usePinned { outPin -> + checkError(EVP_DigestSign(ctx, outPin.safeAddressOfU(0), siglen.ptr, pin.safeAddressOfU(0), data.size.convert())) + } + out.ensureSizeExactly(siglen.value.convert()) + out + } + } finally { + EVP_MD_CTX_free(ctx) + } + } + } + else -> StreamingSignFunction(Resource(checkError(EVP_MD_CTX_new()), ::EVP_MD_CTX_free)) } // inner class to have a reference to class with cleaner - private inner class Openssl3DigestSignFunction( + private inner class StreamingSignFunction( private val context: Resource>, ) : SignFunction, SafeCloseable(SafeCloseAction(context, AutoCloseable::close)) { init { @@ -66,7 +98,7 @@ internal abstract class Openssl3DigestSignatureGenerator( EVP_DigestSignInit_ex( ctx = context, pctx = null, - mdname = hashAlgorithm, + mdname = hashAlgorithm!!, libctx = null, props = null, pkey = privateKey, @@ -75,4 +107,6 @@ internal abstract class Openssl3DigestSignatureGenerator( ) } } + + // One-shot path now handled by AccumulatingSignFunction in createSignFunction } diff --git a/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3DigestSignatureVerifier.kt b/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3DigestSignatureVerifier.kt index 3f6775ab..4db6f40e 100644 --- a/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3DigestSignatureVerifier.kt +++ b/cryptography-providers/openssl3/api/src/commonMain/kotlin/operations/Openssl3DigestSignatureVerifier.kt @@ -8,24 +8,59 @@ import dev.whyoleg.cryptography.operations.* import dev.whyoleg.cryptography.providers.base.* import dev.whyoleg.cryptography.providers.openssl3.internal.* import dev.whyoleg.cryptography.providers.openssl3.internal.cinterop.* +import dev.whyoleg.cryptography.providers.base.operations.* import kotlinx.cinterop.* import kotlin.experimental.* internal abstract class Openssl3DigestSignatureVerifier( private val publicKey: CPointer, - private val hashAlgorithm: String, + // when null, performs one-shot verification (e.g., EdDSA) + private val hashAlgorithm: String?, ) : SignatureVerifier { @OptIn(ExperimentalNativeApi::class) private val cleaner = publicKey.upRef().cleaner() protected abstract fun MemScope.createParams(): CValuesRef? - override fun createVerifyFunction(): VerifyFunction { - return Openssl3DigestVerifyFunction(Resource(checkError(EVP_MD_CTX_new()), ::EVP_MD_CTX_free)) + override fun createVerifyFunction(): VerifyFunction = when (hashAlgorithm) { + null -> AccumulatingVerifyFunction { data, signature, startIndex, endIndex -> + memScoped { + val ctx = checkError(EVP_MD_CTX_new()) + try { + checkError( + EVP_DigestVerifyInit_ex( + ctx = ctx, + pctx = null, + mdname = null, // one-shot mode + libctx = null, + props = null, + pkey = publicKey, + params = createParams() + ) + ) + val result = signature.usePinned { sigPin -> + data.usePinned { dataPin -> + EVP_DigestVerify( + ctx, + sigPin.safeAddressOfU(startIndex), + (endIndex - startIndex).convert(), + dataPin.safeAddressOfU(0), + data.size.convert() + ) + } + } + if (result != 0) checkError(result) + result == 1 + } finally { + EVP_MD_CTX_free(ctx) + } + } + } + else -> StreamingVerifyFunction(Resource(checkError(EVP_MD_CTX_new()), ::EVP_MD_CTX_free)) } // inner class to have a reference to class with cleaner - private inner class Openssl3DigestVerifyFunction( + private inner class StreamingVerifyFunction( private val context: Resource>, ) : VerifyFunction, SafeCloseable(SafeCloseAction(context, AutoCloseable::close)) { init { @@ -67,7 +102,7 @@ internal abstract class Openssl3DigestSignatureVerifier( EVP_DigestVerifyInit_ex( ctx = context, pctx = null, - mdname = hashAlgorithm, + mdname = hashAlgorithm!!, libctx = null, props = null, pkey = publicKey, @@ -76,4 +111,6 @@ internal abstract class Openssl3DigestSignatureVerifier( ) } } + + // One-shot path now handled by AccumulatingVerifyFunction in createVerifyFunction }