From 286b9e3bddcc55f46454b4fa58a9395175d3660d Mon Sep 17 00:00:00 2001 From: Minecon724 Date: Mon, 27 Oct 2025 20:13:00 +0100 Subject: [PATCH] Commit --- build.gradle.kts | 10 ++ .../dn42/m724/auth/AuthenticationMethod.kt | 20 --- src/nativeMain/kotlin/dn42/m724/auth/Main.kt | 23 +--- .../auth/{ => checker}/SignatureChecker.kt | 2 +- .../{ => checker}/pgp/PgpSignatureChecker.kt | 58 +++++++-- .../auth/checker/ssh/SshSignatureChecker.kt | 9 ++ .../m724/auth/model/AuthenticationMethod.kt | 39 ++++++ .../pgp/PgpSignatureVerificationTest.kt | 119 ++++++++++++++++++ .../auth/model/AuthenticationMethodTest.kt | 31 +++++ .../auth/pgp/PgpSignatureVerificationTest.kt | 96 -------------- 10 files changed, 255 insertions(+), 152 deletions(-) delete mode 100644 src/nativeMain/kotlin/dn42/m724/auth/AuthenticationMethod.kt rename src/nativeMain/kotlin/dn42/m724/auth/{ => checker}/SignatureChecker.kt (94%) rename src/nativeMain/kotlin/dn42/m724/auth/{ => checker}/pgp/PgpSignatureChecker.kt (72%) create mode 100644 src/nativeMain/kotlin/dn42/m724/auth/checker/ssh/SshSignatureChecker.kt create mode 100644 src/nativeMain/kotlin/dn42/m724/auth/model/AuthenticationMethod.kt create mode 100644 src/nativeTest/kotlin/dn42/m724/auth/checker/pgp/PgpSignatureVerificationTest.kt create mode 100644 src/nativeTest/kotlin/dn42/m724/auth/model/AuthenticationMethodTest.kt delete mode 100644 src/nativeTest/kotlin/dn42/m724/auth/pgp/PgpSignatureVerificationTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index 60a8f06..d128c36 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,7 @@ plugins { kotlin("multiplatform") version "2.2.21" + id("io.kotest") version "6.0.4" + id("com.google.devtools.ksp") version "2.3.0" } repositories { @@ -36,7 +38,15 @@ kotlin { nativeMain.dependencies { } nativeTest.dependencies { + implementation("io.kotest:kotest-framework-engine:6.0.4") implementation("io.kotest:kotest-assertions-core:6.0.4") + implementation("io.kotest:kotest-property:6.0.4") } } +} + +// note the documentation said "temporarily" so idk https://kotest.io/docs/framework/project-setup.html#re-running-tests +tasks.withType().configureEach { + logger.lifecycle("UP-TO-DATE check for $name is disabled, forcing it to run.") + outputs.upToDateWhen { false } } \ No newline at end of file diff --git a/src/nativeMain/kotlin/dn42/m724/auth/AuthenticationMethod.kt b/src/nativeMain/kotlin/dn42/m724/auth/AuthenticationMethod.kt deleted file mode 100644 index c734f81..0000000 --- a/src/nativeMain/kotlin/dn42/m724/auth/AuthenticationMethod.kt +++ /dev/null @@ -1,20 +0,0 @@ -package dn42.m724.auth - -sealed interface AuthenticationMethod { - data class Pgp(val fingerprint: String) : AuthenticationMethod { - init { - // TODO validate - } - } - - data class Ssh(val type: KeyType, val fingerprint: String) : AuthenticationMethod { - init { - // TODO validate - } - - enum class KeyType { - Rsa, - Ed25519 - } - } -} diff --git a/src/nativeMain/kotlin/dn42/m724/auth/Main.kt b/src/nativeMain/kotlin/dn42/m724/auth/Main.kt index 98051af..a14a1f0 100644 --- a/src/nativeMain/kotlin/dn42/m724/auth/Main.kt +++ b/src/nativeMain/kotlin/dn42/m724/auth/Main.kt @@ -1,26 +1,5 @@ package dn42.m724.auth -import dn42.m724.auth.pgp.PgpSignatureChecker - -// This is actually a library, below code is just showcase - fun main() { - print("Message to sign (single line): ") - val message = readln() - - print("Key fingerprint:") - val fingerprint = readln() - - println("Please input signature below:") - val lines = mutableListOf() - while (lines.lastOrNull() != "-----END PGP SIGNATURE-----") { - print(" ") - lines.add(readln()) - } - - val signature = lines.joinToString("\n") - - val checker = PgpSignatureChecker() - checker.verify(message, fingerprint, signature) - println("Signature is valid") + TODO("usage example or something here") } \ No newline at end of file diff --git a/src/nativeMain/kotlin/dn42/m724/auth/SignatureChecker.kt b/src/nativeMain/kotlin/dn42/m724/auth/checker/SignatureChecker.kt similarity index 94% rename from src/nativeMain/kotlin/dn42/m724/auth/SignatureChecker.kt rename to src/nativeMain/kotlin/dn42/m724/auth/checker/SignatureChecker.kt index 566b9d6..144f419 100644 --- a/src/nativeMain/kotlin/dn42/m724/auth/SignatureChecker.kt +++ b/src/nativeMain/kotlin/dn42/m724/auth/checker/SignatureChecker.kt @@ -1,4 +1,4 @@ -package dn42.m724.auth +package dn42.m724.auth.checker interface SignatureChecker { /** diff --git a/src/nativeMain/kotlin/dn42/m724/auth/pgp/PgpSignatureChecker.kt b/src/nativeMain/kotlin/dn42/m724/auth/checker/pgp/PgpSignatureChecker.kt similarity index 72% rename from src/nativeMain/kotlin/dn42/m724/auth/pgp/PgpSignatureChecker.kt rename to src/nativeMain/kotlin/dn42/m724/auth/checker/pgp/PgpSignatureChecker.kt index c8b95e9..d9729ff 100644 --- a/src/nativeMain/kotlin/dn42/m724/auth/pgp/PgpSignatureChecker.kt +++ b/src/nativeMain/kotlin/dn42/m724/auth/checker/pgp/PgpSignatureChecker.kt @@ -1,16 +1,47 @@ -package dn42.m724.auth.pgp +package dn42.m724.auth.checker.pgp -import dn42.m724.auth.SignatureChecker -import gpgme.* -import kotlinx.cinterop.* +import dn42.m724.auth.checker.SignatureChecker +import gpgme.GPGME_PROTOCOL_OPENPGP +import gpgme.GPGME_SIG_STAT_GOOD +import gpgme.GPG_ERR_NO_ERROR +import gpgme.SEEK_SET +import gpgme._gpgme_op_verify_result +import gpgme._gpgme_signature +import gpgme.gpg_err_code +import gpgme.gpgme_check_version +import gpgme.gpgme_ctx_t +import gpgme.gpgme_ctx_tVar +import gpgme.gpgme_data_new +import gpgme.gpgme_data_new_from_mem +import gpgme.gpgme_data_read +import gpgme.gpgme_data_release +import gpgme.gpgme_data_seek +import gpgme.gpgme_data_tVar +import gpgme.gpgme_error_from_errno +import gpgme.gpgme_new +import gpgme.gpgme_op_verify +import gpgme.gpgme_op_verify_result +import gpgme.gpgme_release +import gpgme.gpgme_set_armor +import gpgme.gpgme_set_protocol +import gpgme.gpgme_strerror +import kotlinx.cinterop.ByteVar +import kotlinx.cinterop.CPointer +import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.MemScope +import kotlinx.cinterop.alloc +import kotlinx.cinterop.allocArray +import kotlinx.cinterop.memScoped +import kotlinx.cinterop.pointed +import kotlinx.cinterop.ptr +import kotlinx.cinterop.toKString +import kotlinx.cinterop.value import platform.posix.errno @OptIn(ExperimentalForeignApi::class) class PgpSignatureChecker : SignatureChecker { override fun verify(sourcePlainText: String, keyFingerprint: String, signature: String) { - gpgme_check_version(null)?.let { - println("GPGME version: ${it.toKString()}") - } ?: throw RuntimeException("Failed to get GPGME version") + gpgme_check_version(null) ?: throw RuntimeException("Failed to get GPGME version") withGpgContextMemScoped { ctx -> gpgme_set_protocol(ctx, GPGME_PROTOCOL_OPENPGP.toUInt()) @@ -41,11 +72,10 @@ class PgpSignatureChecker : SignatureChecker { result.pointed.signatures?.let { initialPointer -> var signaturePointer: CPointer<_gpgme_signature>? = initialPointer - println("pointer is null: ${signaturePointer == null}") while (signaturePointer != null) { val signature = signaturePointer.pointed - println("Found signature with timestamp ${signature.timestamp} and fingerprint ${signature.fpr?.toKString()}") + //println("Found signature with timestamp ${signature.timestamp} and fingerprint ${signature.fpr?.toKString()}") assertGpg(signature.status) { "Failed to check signature" } @@ -74,7 +104,12 @@ class PgpSignatureChecker : SignatureChecker { try { assertGpg( - gpgme_data_new_from_mem(sigData.ptr, signedMessage, signedMessage.length.toULong(), 1) // TODO idk if the 1 is good + gpgme_data_new_from_mem( + sigData.ptr, + signedMessage, + signedMessage.length.toULong(), + 1 + ) // TODO idk if the 1 is good ) { "Failed to read signature" } assertGpg( @@ -101,9 +136,6 @@ class PgpSignatureChecker : SignatureChecker { } } - println(plainText.encodeToByteArray().toHexString()) - println(sourcePlainText.encodeToByteArray().toHexString()) - check(plainText.trim() == sourcePlainText.trim()) { "Plain text message doesn't match" } } } finally { diff --git a/src/nativeMain/kotlin/dn42/m724/auth/checker/ssh/SshSignatureChecker.kt b/src/nativeMain/kotlin/dn42/m724/auth/checker/ssh/SshSignatureChecker.kt new file mode 100644 index 0000000..1897441 --- /dev/null +++ b/src/nativeMain/kotlin/dn42/m724/auth/checker/ssh/SshSignatureChecker.kt @@ -0,0 +1,9 @@ +package dn42.m724.auth.checker.ssh + +import dn42.m724.auth.checker.SignatureChecker + +class SshSignatureChecker : SignatureChecker { + override fun verify(sourcePlainText: String, keyFingerprint: String, signature: String) { + TODO("SSH signature verification not yet implemented") + } +} \ No newline at end of file diff --git a/src/nativeMain/kotlin/dn42/m724/auth/model/AuthenticationMethod.kt b/src/nativeMain/kotlin/dn42/m724/auth/model/AuthenticationMethod.kt new file mode 100644 index 0000000..2639cd4 --- /dev/null +++ b/src/nativeMain/kotlin/dn42/m724/auth/model/AuthenticationMethod.kt @@ -0,0 +1,39 @@ +package dn42.m724.auth.model + +sealed interface AuthenticationMethod { + data class Pgp(val fingerprint: String) : AuthenticationMethod { + init { + // TODO validate + } + } + + data class Ssh(val algorithm: Algorithm, val fingerprint: String) : AuthenticationMethod { + init { + // TODO validate + } + + enum class Algorithm { + Rsa, + Ed25519 + } + } + + companion object { + fun fromString(value: String): AuthenticationMethod { + val (keyType, fingerprint) = value.split(" ") + + val authenticationMethod = if (keyType == "pgp-fingerprint") { + val bytes = fingerprint.hexToByteArray() + check(bytes.size == 20) { "Fingerprint is expected to be a 20-byte HEX-encoded string" } + + Pgp(fingerprint) + } else if (keyType.startsWith("ssh-")) { + TODO("SSH key parsing not yet implemented") + } else { + throw IllegalArgumentException("Invalid signature key type: $keyType") + } + + return authenticationMethod + } + } +} \ No newline at end of file diff --git a/src/nativeTest/kotlin/dn42/m724/auth/checker/pgp/PgpSignatureVerificationTest.kt b/src/nativeTest/kotlin/dn42/m724/auth/checker/pgp/PgpSignatureVerificationTest.kt new file mode 100644 index 0000000..1c6874f --- /dev/null +++ b/src/nativeTest/kotlin/dn42/m724/auth/checker/pgp/PgpSignatureVerificationTest.kt @@ -0,0 +1,119 @@ +package dn42.m724.auth.checker.pgp + +import dn42.m724.auth.checker.SignatureChecker +import io.kotest.assertions.throwables.shouldNotThrowAny +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.datatest.withData + +private const val fingerprint = "2DE81640E41DD84A9ED9B8F8FD4D7C6652E72E7B" +private const val invalidFingerprint = "139F1460BC66A19A2F880D8D47BA020D8EBCC05E" + +private val clearTextSignature = """ + -----BEGIN PGP SIGNED MESSAGE----- + Hash: SHA512 + + simple message + -----BEGIN PGP SIGNATURE----- + + iJEEARYKADkWIQQt6BZA5B3YSp7ZuPj9TXxmUucuewUCaP4wchsUgAAAAAAEAA5t + YW51MiwyLjUrMS4xMSwyLDIACgkQ/U18ZlLnLnuM3AD+L0QBrn2pnGAemcJZDh+p + 6oJnuTgeSMiMRkkbMgTOMFMBAPzhLKgyzx4YJcgrIinvZsgPowR9Pf0ryzxwQ5mo + qUwJ + =FYVj + -----END PGP SIGNATURE----- + """.trimIndent() + +val detachedSignature = """ + -----BEGIN PGP SIGNATURE----- + + iJEEABYKADkWIQQt6BZA5B3YSp7ZuPj9TXxmUucuewUCaP4wWxsUgAAAAAAEAA5t + YW51MiwyLjUrMS4xMSwyLDIACgkQ/U18ZlLnLnsaEgEA15uncZNWN6zV952vO6rG + mMIAG9X54X9Cfp5DEfG3hPcBAJypNGnlyHivk+ZKYDlKSZB2S53vKrA3q3J2yDqb + 0SUF + =12FC + -----END PGP SIGNATURE----- +""".trimIndent() + +class PgpSignatureVerificationTest: FunSpec({ + val checker: SignatureChecker = PgpSignatureChecker() // stateless (for now?) + + val successfulCases = listOf( + TestCase( + description = "valid clear-text signature", + plainText = "simple message", + keyFingerprint = fingerprint, + signature = clearTextSignature + ), + TestCase( + description = "valid detached signature", + plainText = "simple message", + keyFingerprint = fingerprint, + signature = detachedSignature + ) + ) + + withData( + nameFn = { "verify should succeed for ${it.description}" }, + successfulCases + ) { (_, sourcePlainText, keyFingerprint, signature) -> + shouldNotThrowAny { + checker.verify(sourcePlainText, keyFingerprint, signature) + } + } + + val failureCases = listOf( + TestCase( + description = "not matching message (clear-text)", + plainText = "totally different", + keyFingerprint = fingerprint, + signature = clearTextSignature + ), + TestCase( + description = "not matching message (detached)", + plainText = "totally different", + keyFingerprint = fingerprint, + signature = detachedSignature + ), + TestCase( + description = "not matching fingerprint", + plainText = "simple message", + keyFingerprint = invalidFingerprint, + signature = detachedSignature + ), + TestCase( + description = "blank signature", + plainText = "simple message", + keyFingerprint = fingerprint, + signature = "" + ), + TestCase( + description = "blank fingerprint", + plainText = "simple message", + keyFingerprint = "", + signature = detachedSignature + ), + TestCase( + description = "key ID instead of full fingerprint", + plainText = "simple message", + keyFingerprint = fingerprint.takeLast(16), // Assuming this is an invalid case + signature = detachedSignature + ) + ) + + withData( + nameFn = { "verify should throw for ${it.description}" }, + failureCases + ) { (_, sourcePlainText, keyFingerprint, signature) -> + shouldThrow { // TODO we should check the exception type when we start using exception types + checker.verify(sourcePlainText, keyFingerprint, signature) + } + } +}) + +private data class TestCase( + val description: String, + val plainText: String, + val keyFingerprint: String, + val signature: String +) \ No newline at end of file diff --git a/src/nativeTest/kotlin/dn42/m724/auth/model/AuthenticationMethodTest.kt b/src/nativeTest/kotlin/dn42/m724/auth/model/AuthenticationMethodTest.kt new file mode 100644 index 0000000..3bf360e --- /dev/null +++ b/src/nativeTest/kotlin/dn42/m724/auth/model/AuthenticationMethodTest.kt @@ -0,0 +1,31 @@ +package dn42.m724.auth.model + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.datatest.withData +import kotlin.test.assertEquals + +class AuthenticationMethodTest: FunSpec({ + withData( + nameFn = { authenticationMethodString -> "fromString should throw for invalid input: $authenticationMethodString" }, + "pgp-fingerprint 2DE81640E41D", + "pgp 2DE81640E41DD84A9ED9B8F8FD4D7C6652E72E7B", + "pgp-fingerprint 2DE81640E41DD84A9ED9B8F8FD4xveTes7C6652E72E7B", + "pgp-fingerprint" + ) { authenticationMethodString -> + shouldThrow { + AuthenticationMethod.fromString(authenticationMethodString) + } + } + + withData( + nameFn = { (authenticationMethodString, expected) -> "fromString should return $expected for input: $authenticationMethodString" }, + Pair("pgp-fingerprint 2DE81640E41DD84A9ED9B8F8FD4D7C6652E72E7B", AuthenticationMethod.Pgp("2DE81640E41DD84A9ED9B8F8FD4D7C6652E72E7B")), + Pair("pgp-fingerprint 2DE81640E41DD84A9ED9B8F8FD4D7C6652E72E7B this is ignored", AuthenticationMethod.Pgp("2DE81640E41DD84A9ED9B8F8FD4D7C6652E72E7B")) + ) { (authenticationMethodString, expected) -> + assertEquals( + expected = expected, + actual = AuthenticationMethod.fromString(authenticationMethodString) + ) + } +}) \ No newline at end of file diff --git a/src/nativeTest/kotlin/dn42/m724/auth/pgp/PgpSignatureVerificationTest.kt b/src/nativeTest/kotlin/dn42/m724/auth/pgp/PgpSignatureVerificationTest.kt deleted file mode 100644 index 1864c52..0000000 --- a/src/nativeTest/kotlin/dn42/m724/auth/pgp/PgpSignatureVerificationTest.kt +++ /dev/null @@ -1,96 +0,0 @@ -package dn42.m724.auth.pgp - -import dn42.m724.auth.SignatureChecker -import io.kotest.assertions.throwables.shouldNotThrowAny -import io.kotest.assertions.throwables.shouldThrow -import kotlin.experimental.ExperimentalNativeApi -import kotlin.test.BeforeTest -import kotlin.test.Test - -private const val fingerprint = "2DE81640E41DD84A9ED9B8F8FD4D7C6652E72E7B" -private const val invalidFingerprint = "139F1460BC66A19A2F880D8D47BA020D8EBCC05E" - -private val clearTextSignature = """ - -----BEGIN PGP SIGNED MESSAGE----- - Hash: SHA512 - - simple message - -----BEGIN PGP SIGNATURE----- - - iJEEARYKADkWIQQt6BZA5B3YSp7ZuPj9TXxmUucuewUCaP4wchsUgAAAAAAEAA5t - YW51MiwyLjUrMS4xMSwyLDIACgkQ/U18ZlLnLnuM3AD+L0QBrn2pnGAemcJZDh+p - 6oJnuTgeSMiMRkkbMgTOMFMBAPzhLKgyzx4YJcgrIinvZsgPowR9Pf0ryzxwQ5mo - qUwJ - =FYVj - -----END PGP SIGNATURE----- - """.trimIndent() - -val detachedSignature = """ - -----BEGIN PGP SIGNATURE----- - - iJEEABYKADkWIQQt6BZA5B3YSp7ZuPj9TXxmUucuewUCaP4wWxsUgAAAAAAEAA5t - YW51MiwyLjUrMS4xMSwyLDIACgkQ/U18ZlLnLnsaEgEA15uncZNWN6zV952vO6rG - mMIAG9X54X9Cfp5DEfG3hPcBAJypNGnlyHivk+ZKYDlKSZB2S53vKrA3q3J2yDqb - 0SUF - =12FC - -----END PGP SIGNATURE----- -""".trimIndent() - -@OptIn(ExperimentalNativeApi::class) -class PgpSignatureVerificationTest { - private lateinit var checker: SignatureChecker - - @BeforeTest - fun beforeTest() { - checker = PgpSignatureChecker() - } - - @Test - fun `verify should succeed for valid signature`() { - shouldNotThrowAny { - checker.verify ("simple message", fingerprint, clearTextSignature) - } - } - - @Test - fun `verify should succeed for valid detached signature`() { - shouldNotThrowAny { - checker.verify("simple message", fingerprint, detachedSignature) - } - } - - @Test - fun `verify should fail for not matching message`() { - shouldThrow { // "Plain text message doesn't match" - checker.verify("totally different", fingerprint, clearTextSignature) - } - } - - @Test - fun `verify should fail for not matching detached message`() { - shouldThrow { // "Failed to check signature: Bad signature" - checker.verify("totally different", fingerprint, detachedSignature) - } - } - - @Test - fun `verify should fail for not matching fingerprint`() { - shouldThrow { // Did not find any signature with matching key fingerprint - checker.verify("simple message", invalidFingerprint, detachedSignature) - } - } - - @Test - fun `verify should fail for empty input data`() { - shouldThrow { - checker.verify("simple message", invalidFingerprint, "") - } - } - - @Test - fun `verify should fail for empty fingerprint data`() { - shouldThrow { - checker.verify("simple message", "", detachedSignature) - } - } -} \ No newline at end of file