Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ abstract class GenerateProviderTestsTask : DefaultTask() {
@get:Input
abstract val imports: ListProperty<String>

@get:Input
abstract val testClasses: ListProperty<String>

@get:Input
abstract val providerInitializers: MapProperty<String, String>

Expand All @@ -29,11 +32,13 @@ abstract class GenerateProviderTestsTask : DefaultTask() {
check(outputDirectory.deleteRecursively()) { "Failed to cleanup files" }
check(outputDirectory.mkdirs()) { "Failed to create directories" }

val classes = testClasses.get()
providerInitializers.get().forEach { (classifier, providerInitialization) ->
outputDirectory.resolve("${classifier}_tests.kt").writeText(
testsFileContent(
packageName = packageName.get(),
imports = imports.get(),
testClasses = classes,
providerClassifier = classifier,
providerInitialization = providerInitialization
)
Expand All @@ -44,6 +49,7 @@ abstract class GenerateProviderTestsTask : DefaultTask() {
private fun testsFileContent(
packageName: String,
imports: List<String>,
testClasses: List<String>,
providerClassifier: String,
providerInitialization: String,
): String = buildString {
Expand Down Expand Up @@ -110,6 +116,12 @@ abstract class GenerateProviderTestsTask : DefaultTask() {
"EcdsaCompatibilityTest",
"EcdhCompatibilityTest",

// Edwards-family
"EdDsaTest",
"EdDsaCompatibilityTest",
"XdhTest",
"XdhCompatibilityTest",

"RsaOaepTest",
"RsaOaepCompatibilityTest",
"RsaPkcs1Test",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.cryptography.providers.tests.compatibility

import dev.whyoleg.cryptography.*
import dev.whyoleg.cryptography.algorithms.*
import dev.whyoleg.cryptography.providers.tests.*
import dev.whyoleg.cryptography.providers.tests.compatibility.api.*
import dev.whyoleg.cryptography.random.*
import dev.whyoleg.cryptography.serialization.pem.*
import kotlinx.io.bytestring.*
import kotlinx.serialization.*
import kotlin.test.*

private val edPublicKeyFormats = listOf(
EdDSA.PublicKey.Format.JWK,
EdDSA.PublicKey.Format.RAW,
EdDSA.PublicKey.Format.DER,
EdDSA.PublicKey.Format.PEM,
).associateBy { it.name }

private val edPrivateKeyFormats = listOf(
EdDSA.PrivateKey.Format.JWK,
EdDSA.PrivateKey.Format.RAW,
EdDSA.PrivateKey.Format.DER,
EdDSA.PrivateKey.Format.PEM,
).associateBy { it.name }

abstract class EdDsaCompatibilityTest(
provider: CryptographyProvider,
) : CompatibilityTest<EdDSA>(EdDSA, provider) {

@Serializable
private data class KeyParameters(val curveName: String) : TestParameters {
val curve: EdDSA.Curve
get() = when (curveName) {
EdDSA.Curve.Ed25519.name -> EdDSA.Curve.Ed25519
EdDSA.Curve.Ed448.name -> EdDSA.Curve.Ed448
else -> error("Unsupported curve: $curveName")
}
}

override suspend fun CompatibilityTestScope<EdDSA>.generate(isStressTest: Boolean) {
val signatureIterations = if (isStressTest) 5 else 2

listOf(EdDSA.Curve.Ed25519, EdDSA.Curve.Ed448).forEach { curve ->
if (!supportsAlgorithmOnCurve(curve)) return@forEach

val keyParametersId = api.keyPairs.saveParameters(KeyParameters(curve.name))

val keyIterations = if (isStressTest) 5 else 2
algorithm.keyPairGenerator(curve).generateKeys(keyIterations) { keyPair ->
val keyReference = api.keyPairs.saveData(
keyParametersId,
KeyPairData(
public = KeyData(keyPair.publicKey.encodeTo(edPublicKeyFormats.values, ::supportsKeyFormat)),
private = KeyData(keyPair.privateKey.encodeTo(edPrivateKeyFormats.values, ::supportsKeyFormat)),
)
)

repeat(signatureIterations) {
val dataSize = CryptographyRandom.nextInt(0, 8192)
val data = ByteString(CryptographyRandom.nextBytes(dataSize))
val signature = keyPair.privateKey.signatureGenerator().generateSignature(data)

api.signatures.saveData(
parametersId = api.signatures.saveParameters(TestParameters.Empty),
data = SignatureData(keyReference, data, signature)
)
}
}
}
}

private fun ProviderTestScope.supportsAlgorithmOnCurve(curve: EdDSA.Curve): Boolean {
return when {
// CryptoKit supports only Ed25519
context.provider.isCryptoKit && curve == EdDSA.Curve.Ed448 -> {
logger.print("SKIP: CryptoKit supports Ed25519 only")
false
}
// WebCrypto currently supports Ed25519 but not Ed448
context.provider.isWebCrypto && curve == EdDSA.Curve.Ed448 -> {
logger.print("SKIP: 'Ed448' is not supported")
false
}
else -> true
}
}

override suspend fun CompatibilityTestScope<EdDSA>.validate() {
// Decode saved keys
val keyPairs = buildMap {
api.keyPairs.getParameters<KeyParameters> { params, parametersId, _ ->
val publicKeyDecoder = algorithm.publicKeyDecoder(params.curve)
val privateKeyDecoder = algorithm.privateKeyDecoder(params.curve)
api.keyPairs.getData<KeyPairData>(parametersId) { (public, private), keyReference, _ ->
val publicKeys = publicKeyDecoder.decodeFrom(
formats = public.formats,
formatOf = edPublicKeyFormats::getValue,
supports = ::supportsKeyFormat
) { key, format, bytes ->
when (format) {
EdDSA.PublicKey.Format.JWK -> {}
EdDSA.PublicKey.Format.PEM -> {
val expected = PemDocument.decode(bytes)
val actual = PemDocument.decode(key.encodeToByteString(format))
assertEquals(expected.label, actual.label)
assertEquals(PemLabel.PublicKey, actual.label)
assertContentEquals(expected.content, actual.content, "Public Key $format content encoding")
}
else -> assertContentEquals(bytes, key.encodeToByteString(format), "Public Key $format encoding")
}
}
val privateKeys = privateKeyDecoder.decodeFrom(
formats = private.formats,
formatOf = edPrivateKeyFormats::getValue,
supports = ::supportsKeyFormat
) { key, format, bytes ->
when (format) {
EdDSA.PrivateKey.Format.JWK -> {}
EdDSA.PrivateKey.Format.PEM -> {
val expected = PemDocument.decode(bytes)
val actual = PemDocument.decode(key.encodeToByteString(format))
assertEquals(expected.label, actual.label)
assertEquals(PemLabel.PrivateKey, actual.label)
assertContentEquals(expected.content, actual.content, "Private Key $format content encoding")
}
else -> assertContentEquals(bytes, key.encodeToByteString(format), "Private Key $format encoding")
}
}
put(keyReference, publicKeys to privateKeys)
}
}
}

// Validate signatures across providers
api.signatures.getParameters<TestParameters.Empty> { _, parametersId, _ ->
api.signatures.getData<SignatureData>(parametersId) { (keyReference, data, signature), _, _ ->
val (publicKeys, privateKeys) = keyPairs[keyReference] ?: return@getData
val verifiers = publicKeys.map { it.signatureVerifier() }
val generators = privateKeys.map { it.signatureGenerator() }

verifiers.forEach { verifier ->
assertTrue(verifier.tryVerifySignature(data, signature), "Verify")
generators.forEach { generator ->
val s = generator.generateSignature(data)
assertTrue(verifier.tryVerifySignature(data, s), "Sign-Verify")
}
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
* Copyright (c) 2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.whyoleg.cryptography.providers.tests.compatibility

import dev.whyoleg.cryptography.*
import dev.whyoleg.cryptography.algorithms.*
import dev.whyoleg.cryptography.providers.tests.*
import dev.whyoleg.cryptography.providers.tests.compatibility.api.*
import dev.whyoleg.cryptography.serialization.pem.*
import kotlinx.serialization.*
import kotlin.test.*

private val xdhPublicKeyFormats = listOf(
XDH.PublicKey.Format.JWK,
XDH.PublicKey.Format.RAW,
XDH.PublicKey.Format.DER,
XDH.PublicKey.Format.PEM,
).associateBy { it.name }

private val xdhPrivateKeyFormats = listOf(
XDH.PrivateKey.Format.JWK,
XDH.PrivateKey.Format.RAW,
XDH.PrivateKey.Format.DER,
XDH.PrivateKey.Format.PEM,
).associateBy { it.name }

abstract class XdhCompatibilityTest(
provider: CryptographyProvider,
) : CompatibilityTest<XDH>(XDH, provider) {

@Serializable
private data class KeyParameters(val curveName: String) : TestParameters {
val curve: XDH.Curve
get() = when (curveName) {
XDH.Curve.X25519.name -> XDH.Curve.X25519
XDH.Curve.X448.name -> XDH.Curve.X448
else -> error("Unsupported curve: $curveName")
}
}

override suspend fun CompatibilityTestScope<XDH>.generate(isStressTest: Boolean) {
val parametersId = api.sharedSecrets.saveParameters(TestParameters.Empty)

listOf(XDH.Curve.X25519, XDH.Curve.X448).forEach { curve ->
// CryptoKit supports only X25519
if (context.provider.isCryptoKit && curve == XDH.Curve.X448) return@forEach
// WebCrypto currently supports X25519 but not X448
if (context.provider.isWebCrypto && curve == XDH.Curve.X448) {
logger.print("SKIP: 'X448' is not supported")
return@forEach
}
val keyParametersId = api.keyPairs.saveParameters(KeyParameters(curve.name))

val keyIterations = if (isStressTest) 5 else 2
// Generate two key pairs for shared secret validation
algorithm.keyPairGenerator(curve).generateKeys(keyIterations) { keyPair ->
val keyReference = api.keyPairs.saveData(
keyParametersId,
KeyPairData(
public = KeyData(keyPair.publicKey.encodeTo(xdhPublicKeyFormats.values, ::supportsKeyFormat)),
private = KeyData(keyPair.privateKey.encodeTo(xdhPrivateKeyFormats.values, ::supportsKeyFormat))
)
)

algorithm.keyPairGenerator(curve).generateKeys(1) { otherKeyPair ->
val otherKeyReference = api.keyPairs.saveData(
keyParametersId,
KeyPairData(
public = KeyData(otherKeyPair.publicKey.encodeTo(xdhPublicKeyFormats.values, ::supportsKeyFormat)),
private = KeyData(otherKeyPair.privateKey.encodeTo(xdhPrivateKeyFormats.values, ::supportsKeyFormat))
)
)

val shared = keyPair.privateKey.sharedSecretGenerator().generateSharedSecret(otherKeyPair.publicKey)
api.sharedSecrets.saveData(parametersId, SharedSecretData(keyReference, otherKeyReference, shared))
}
}
}
}

override suspend fun CompatibilityTestScope<XDH>.validate() {
val keyPairs = buildMap {
api.keyPairs.getParameters<KeyParameters> { params, parametersId, _ ->
val publicKeyDecoder = algorithm.publicKeyDecoder(params.curve)
val privateKeyDecoder = algorithm.privateKeyDecoder(params.curve)
api.keyPairs.getData<KeyPairData>(parametersId) { (public, private), keyReference, _ ->
val publicKeys = publicKeyDecoder.decodeFrom(
formats = public.formats,
formatOf = xdhPublicKeyFormats::getValue,
supports = ::supportsKeyFormat
) { key, format, bytes ->
when (format) {
XDH.PublicKey.Format.PEM -> {
val expected = PemDocument.decode(bytes)
val actual = PemDocument.decode(key.encodeToByteString(format))
assertEquals(expected.label, actual.label)
assertEquals(PemLabel.PublicKey, actual.label)
assertContentEquals(expected.content, actual.content, "Public Key $format content encoding")
}
else -> assertContentEquals(bytes, key.encodeToByteString(format), "Public Key $format encoding")
}
}
val privateKeys = privateKeyDecoder.decodeFrom(
formats = private.formats,
formatOf = xdhPrivateKeyFormats::getValue,
supports = ::supportsKeyFormat
) { key, format, bytes ->
when (format) {
XDH.PrivateKey.Format.PEM -> {
val expected = PemDocument.decode(bytes)
val actual = PemDocument.decode(key.encodeToByteString(format))
assertEquals(expected.label, actual.label)
assertEquals(PemLabel.PrivateKey, actual.label)
assertContentEquals(expected.content, actual.content, "Private Key $format content encoding")
}
else -> assertContentEquals(bytes, key.encodeToByteString(format), "Private Key $format encoding")
}
}
put(keyReference, publicKeys to privateKeys)
}
}
}

api.sharedSecrets.getParameters<TestParameters.Empty> { _, parametersId, _ ->
api.sharedSecrets.getData<SharedSecretData>(parametersId) { (keyReference, otherKeyReference, sharedSecret), _, _ ->
val (publicKeys, privateKeys) = keyPairs[keyReference] ?: return@getData
val (otherPublicKeys, otherPrivateKeys) = keyPairs[otherKeyReference] ?: return@getData

// Verify both combinations generate the same secret
publicKeys.forEach { publicKey ->
otherPrivateKeys.forEach { otherPrivateKey ->
assertContentEquals(sharedSecret, publicKey.sharedSecretGenerator().generateSharedSecret(otherPrivateKey))
assertContentEquals(sharedSecret, otherPrivateKey.sharedSecretGenerator().generateSharedSecret(publicKey))
}
}
privateKeys.forEach { privateKey ->
otherPublicKeys.forEach { otherPublicKey ->
assertContentEquals(sharedSecret, privateKey.sharedSecretGenerator().generateSharedSecret(otherPublicKey))
assertContentEquals(sharedSecret, otherPublicKey.sharedSecretGenerator().generateSharedSecret(privateKey))
}
}
}
}
}
}
Loading