From 7d0a974b722fcbdce86917bbca2f2f6499101411 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Thu, 21 May 2026 22:07:59 -0700 Subject: [PATCH] chore: add tests for SignatureVerifier Also trim out testapp/* to keep code coverage up. --- build.gradle.kts | 1 + .../sshlib/crypto/SignatureVerifierTest.kt | 86 +++++++++++++++++++ 2 files changed, 87 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 004dfd5..2bd2083 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -46,6 +46,7 @@ sonar { property("sonar.organization", "connectbot") property("sonar.host.url", "https://sonarcloud.io") property("sonar.coverage.jacoco.xmlReportPaths", "build/reports/kover/report.xml") + property("sonar.coverage.exclusions", "testapp/**") property("sonar.exclusions", "**/build/generated/**") property("sonar.issue.ignore.multicriteria", "cognitiveComplexityConnection,cognitiveComplexitySftp,cognitiveComplexityTransport") property("sonar.issue.ignore.multicriteria.cognitiveComplexityConnection.ruleKey", "kotlin:S3776") diff --git a/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/SignatureVerifierTest.kt b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/SignatureVerifierTest.kt index 8364b7e..bac83a2 100644 --- a/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/SignatureVerifierTest.kt +++ b/sshlib/src/test/kotlin/org/connectbot/sshlib/crypto/SignatureVerifierTest.kt @@ -59,6 +59,14 @@ class SignatureVerifierTest { return buf.toByteArray() } + private fun buildUnknownHostKey(algorithmName: String = "unknown-key@example.com"): ByteArray { + val buf = ByteArrayOutputStream() + val out = DataOutputStream(buf) + encodeString(out, algorithmName) + encodeString(out, byteArrayOf(1, 2, 3)) + return buf.toByteArray() + } + private fun signData(data: ByteArray, jcaAlgorithm: String, kp: java.security.KeyPair): ByteArray { val sig = Signature.getInstance(jcaAlgorithm) sig.initSign(kp.private) @@ -66,6 +74,20 @@ class SignatureVerifierTest { return sig.sign() } + private fun readKey(resourcePath: String): SshPrivateKey { + val data = requireNotNull(javaClass.getResourceAsStream("/keys/$resourcePath")) { + "Missing test key resource: /keys/$resourcePath" + }.use { stream -> + stream.bufferedReader().use { reader -> reader.readText() } + } + return PrivateKeyReader.read(data) + } + + private fun signWithSshAlgorithm(privateKey: SshPrivateKey, algorithmName: String, data: ByteArray): ByteArray { + val entry = SignatureEntry.fromSshName(algorithmName) ?: error("Unknown algorithm: $algorithmName") + return entry.algorithm.sign(algorithmName, privateKey.jcaKeyPair.private, data) + } + @Test fun `accepts rsa-sha2-256 signature when rsa-sha2-256 was negotiated`() { val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.generateKeyPair() @@ -132,4 +154,68 @@ class SignatureVerifierTest { assertFalse(SignatureVerifier.verify(hostKey, sigBlob, data, "rsa-sha2-256")) } + + @Test + fun `rejects negotiated unknown signature algorithm`() { + val kp = KeyPairGenerator.getInstance("RSA").apply { initialize(2048) }.generateKeyPair() + val data = "exchange hash".toByteArray() + val hostKey = buildRsaHostKey(kp.public as RSAPublicKey) + val sigBlob = buildSignatureBlob("unknown-algo", byteArrayOf(1, 2, 3)) + + assertFalse(SignatureVerifier.verify(hostKey, sigBlob, data, "unknown-algo")) + } + + @Test + fun `verifyWithKeyType accepts RSA-compatible signature algorithms`() { + val privateKey = readKey("rsa_unencrypted") + val data = "session binding".toByteArray() + val hostKey = SshPublicKeyEncoder.encode(privateKey.jcaKeyPair, privateKey.keyType) + + for (algorithmName in listOf("ssh-rsa", "rsa-sha2-256", "rsa-sha2-512")) { + val sigBlob = signWithSshAlgorithm(privateKey, algorithmName, data) + + assertTrue(SignatureVerifier.verifyWithKeyType(hostKey, sigBlob, data), algorithmName) + } + } + + @Test + fun `verifyWithKeyType accepts matching Ed25519 signature algorithm`() { + val privateKey = readKey("ed25519_unencrypted") + val data = "session binding".toByteArray() + val hostKey = SshPublicKeyEncoder.encode(privateKey.jcaKeyPair, privateKey.keyType) + val sigBlob = signWithSshAlgorithm(privateKey, "ssh-ed25519", data) + + assertTrue(SignatureVerifier.verifyWithKeyType(hostKey, sigBlob, data)) + } + + @Test + fun `verifyWithKeyType rejects signature algorithm incompatible with key type`() { + val privateKey = readKey("ed25519_unencrypted") + val rsaKey = readKey("rsa_unencrypted") + val data = "session binding".toByteArray() + val ed25519HostKey = SshPublicKeyEncoder.encode(privateKey.jcaKeyPair, privateKey.keyType) + val rsaSigBlob = signWithSshAlgorithm(rsaKey, "rsa-sha2-256", data) + + assertFalse(SignatureVerifier.verifyWithKeyType(ed25519HostKey, rsaSigBlob, data)) + } + + @Test + fun `verifyWithKeyType rejects non-RSA signature algorithm for RSA key`() { + val rsaKey = readKey("rsa_unencrypted") + val ed25519Key = readKey("ed25519_unencrypted") + val data = "session binding".toByteArray() + val rsaHostKey = SshPublicKeyEncoder.encode(rsaKey.jcaKeyPair, rsaKey.keyType) + val ed25519SigBlob = signWithSshAlgorithm(ed25519Key, "ssh-ed25519", data) + + assertFalse(SignatureVerifier.verifyWithKeyType(rsaHostKey, ed25519SigBlob, data)) + } + + @Test + fun `verifyWithKeyType rejects unknown self-described signature algorithm`() { + val data = "session binding".toByteArray() + val hostKey = buildUnknownHostKey() + val sigBlob = buildSignatureBlob("unknown-key@example.com", byteArrayOf(4, 5, 6)) + + assertFalse(SignatureVerifier.verifyWithKeyType(hostKey, sigBlob, data)) + } }