Commit
This commit is contained in:
parent
5a3e48d070
commit
286b9e3bdd
10 changed files with 255 additions and 152 deletions
|
|
@ -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<Test>().configureEach {
|
||||
logger.lifecycle("UP-TO-DATE check for $name is disabled, forcing it to run.")
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<String>()
|
||||
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")
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package dn42.m724.auth
|
||||
package dn42.m724.auth.checker
|
||||
|
||||
interface SignatureChecker {
|
||||
/**
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<Exception> { // 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
|
||||
)
|
||||
|
|
@ -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<Exception> {
|
||||
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)
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
@ -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<Exception> { // "Plain text message doesn't match"
|
||||
checker.verify("totally different", fingerprint, clearTextSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verify should fail for not matching detached message`() {
|
||||
shouldThrow<Exception> { // "Failed to check signature: Bad signature"
|
||||
checker.verify("totally different", fingerprint, detachedSignature)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verify should fail for not matching fingerprint`() {
|
||||
shouldThrow<Exception> { // 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<Exception> {
|
||||
checker.verify("simple message", invalidFingerprint, "")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `verify should fail for empty fingerprint data`() {
|
||||
shouldThrow<Exception> {
|
||||
checker.verify("simple message", "", detachedSignature)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue