From 94376f8eff51b9b3c88bafe2ad1c193f4d8880ab Mon Sep 17 00:00:00 2001 From: usbharu <64310155+usbharu@users.noreply.github.com> Date: Wed, 18 Oct 2023 12:15:51 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20hs2019=E3=81=AE=E6=A4=9C=E8=A8=BC?= =?UTF-8?q?=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../verify/DefaultSignatureHeaderParser.kt | 10 +- .../verify/Hs2019HttpSignatureVerifier.kt | 107 +++++ .../verify/HttpSignatureVerifier.kt | 1 - ...l.kt => RsaSha256HttpSignatureVerifier.kt} | 2 +- .../usbharu/httpsignature/verify/Signature.kt | 9 +- .../verify/Hs2019HttpSignatureVerifierTest.kt | 417 ++++++++++++++++++ ... => RsaSha256HttpSignatureVerifierTest.kt} | 8 +- 7 files changed, 540 insertions(+), 14 deletions(-) create mode 100644 src/main/kotlin/dev/usbharu/httpsignature/verify/Hs2019HttpSignatureVerifier.kt rename src/main/kotlin/dev/usbharu/httpsignature/verify/{HttpSignatureVerifierImpl.kt => RsaSha256HttpSignatureVerifier.kt} (97%) create mode 100644 src/test/kotlin/dev/usbharu/httpsignature/verify/Hs2019HttpSignatureVerifierTest.kt rename src/test/kotlin/dev/usbharu/httpsignature/verify/{HttpSignatureVerifierImplTest.kt => RsaSha256HttpSignatureVerifierTest.kt} (95%) diff --git a/src/main/kotlin/dev/usbharu/httpsignature/verify/DefaultSignatureHeaderParser.kt b/src/main/kotlin/dev/usbharu/httpsignature/verify/DefaultSignatureHeaderParser.kt index be8953b..4d2dd3f 100644 --- a/src/main/kotlin/dev/usbharu/httpsignature/verify/DefaultSignatureHeaderParser.kt +++ b/src/main/kotlin/dev/usbharu/httpsignature/verify/DefaultSignatureHeaderParser.kt @@ -11,11 +11,13 @@ class DefaultSignatureHeaderParser : SignatureHeaderParser { .map { it.trim('"') } .map { it.split("=\"") } .associate { it[0].split(" ").last() to it[1].trim('"') } + .toMutableMap() return Signature( - parameters.getValue("keyId"), - parameters.getValue("algorithm"), - parameters.getValue("headers").split(" "), - parameters.getValue("signature") + parameters.remove("keyId")!!, + parameters.remove("algorithm")!!, + parameters.remove("headers")?.split(" ")!!, + parameters.remove("signature")!!, + parameters ) } } diff --git a/src/main/kotlin/dev/usbharu/httpsignature/verify/Hs2019HttpSignatureVerifier.kt b/src/main/kotlin/dev/usbharu/httpsignature/verify/Hs2019HttpSignatureVerifier.kt new file mode 100644 index 0000000..c6f075e --- /dev/null +++ b/src/main/kotlin/dev/usbharu/httpsignature/verify/Hs2019HttpSignatureVerifier.kt @@ -0,0 +1,107 @@ +package dev.usbharu.httpsignature.verify + +import dev.usbharu.httpsignature.common.HttpHeaders +import dev.usbharu.httpsignature.common.HttpMethod +import dev.usbharu.httpsignature.common.HttpRequest +import dev.usbharu.httpsignature.common.PublicKey +import java.net.URL +import java.security.spec.MGF1ParameterSpec +import java.security.spec.PSSParameterSpec +import java.time.Instant +import java.util.* + +class Hs2019HttpSignatureVerifier( + private val signatureHeaderParser: SignatureHeaderParser, + private val salt: Int = 64 +) : HttpSignatureVerifier { + override fun verify(httpRequest: HttpRequest, key: PublicKey): VerificationResult { + val signature = signatureHeaderParser.parse(httpRequest.headers) + if (signature.algorithm.equals("hs2019", true).not()) { + return FailedVerification("Unsupported algorithm : ${signature.algorithm}") + } + + if (signature.keyId != key.keyId) { + return FailedVerification("The keyId is different.") + } + + val created = if (signature.headers.contains("(created)")) { + val created = signature.additionalData["created"] + ?: return FailedVerification("(created) header is provided, but it does not exist.") + val l = created.toLongOrNull() ?: return FailedVerification("(created) is an unsupported format.") + if (Instant.ofEpochSecond(l) >= Instant.now()) { + return FailedVerification("(created) is the future.") + } + l + } else { + null + } + + val expires = if (signature.headers.contains("(expires)")) { + val expires = (signature.additionalData["expires"] + ?: return FailedVerification("(expires) header is provided, but it does not exist.")) + val l = expires.toLongOrNull() ?: return FailedVerification("(expires) is an unsupported format.") + if (Instant.ofEpochSecond(l) <= Instant.now()) { + return FailedVerification("(expires) is expired.") + } + l + } else { + null + } + + + val byteSignature = Base64.getDecoder().decode(signature.signature) + + + val buildSignString = + buildSignString( + url = httpRequest.url, + method = httpRequest.method, + headers = httpRequest.headers, + signHeaders = signature.headers, + created = created, + expires = expires + ) + + val verifier = java.security.Signature.getInstance("RSASSA-PSS") + verifier.setParameter(PSSParameterSpec("SHA-512", "MGF1", MGF1ParameterSpec.SHA512, salt, 1)) + verifier.initVerify(key.publicKey) + verifier.update(buildSignString.toByteArray()) + val verify = verifier.verify(byteSignature) + + if (verify) { + return SuccessfulVerification() + } + return FailedVerification("Signature verification failed.") + } + + private fun generalHeader(fieldName: String, value: List): String = "$fieldName: ${value.first()}" + + private fun buildSignString( + url: URL, + method: HttpMethod, + headers: HttpHeaders, + signHeaders: List, + created: Long?, + expires: Long? + ): String { + return signHeaders.joinToString("\n") { + when (it) { + "(request-target)" -> { + "(request-target): ${method.value.lowercase()} ${url.path}" + } + + "(created)" -> { + "(created): ${created!!}" + } + + "(expires)" -> { + "(expires): ${expires!!}" + } + + else -> { + generalHeader(it, headers.get(it)!!) + } + } + } + } +} diff --git a/src/main/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifier.kt b/src/main/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifier.kt index 75e2b66..d6dd0d9 100644 --- a/src/main/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifier.kt +++ b/src/main/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifier.kt @@ -1,7 +1,6 @@ package dev.usbharu.httpsignature.verify import dev.usbharu.httpsignature.common.HttpRequest -import dev.usbharu.httpsignature.common.PrivateKey import dev.usbharu.httpsignature.common.PublicKey interface HttpSignatureVerifier { diff --git a/src/main/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifierImpl.kt b/src/main/kotlin/dev/usbharu/httpsignature/verify/RsaSha256HttpSignatureVerifier.kt similarity index 97% rename from src/main/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifierImpl.kt rename to src/main/kotlin/dev/usbharu/httpsignature/verify/RsaSha256HttpSignatureVerifier.kt index 1ea406e..8d4d3d0 100644 --- a/src/main/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifierImpl.kt +++ b/src/main/kotlin/dev/usbharu/httpsignature/verify/RsaSha256HttpSignatureVerifier.kt @@ -6,7 +6,7 @@ import dev.usbharu.httpsignature.sign.HttpSignatureSigner import java.security.Signature import java.util.* -class HttpSignatureVerifierImpl( +class RsaSha256HttpSignatureVerifier( private val signatureHeaderParser: SignatureHeaderParser, private val httpSignatureSigner: HttpSignatureSigner ) : HttpSignatureVerifier { diff --git a/src/main/kotlin/dev/usbharu/httpsignature/verify/Signature.kt b/src/main/kotlin/dev/usbharu/httpsignature/verify/Signature.kt index 3661c7e..6199356 100644 --- a/src/main/kotlin/dev/usbharu/httpsignature/verify/Signature.kt +++ b/src/main/kotlin/dev/usbharu/httpsignature/verify/Signature.kt @@ -1,8 +1,9 @@ package dev.usbharu.httpsignature.verify data class Signature( - val keyId:String, - val algorithm:String, - val headers:List, - val signature:String + val keyId: String, + val algorithm: String, + val headers: List, + val signature: String, + val additionalData: Map = emptyMap() ) diff --git a/src/test/kotlin/dev/usbharu/httpsignature/verify/Hs2019HttpSignatureVerifierTest.kt b/src/test/kotlin/dev/usbharu/httpsignature/verify/Hs2019HttpSignatureVerifierTest.kt new file mode 100644 index 0000000..880c99f --- /dev/null +++ b/src/test/kotlin/dev/usbharu/httpsignature/verify/Hs2019HttpSignatureVerifierTest.kt @@ -0,0 +1,417 @@ +package dev.usbharu.httpsignature.verify + +import dev.usbharu.httpsignature.common.* +import dev.usbharu.httpsignature.sign.Hs2019HttpSignatureSigner +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertInstanceOf +import org.junit.jupiter.api.Test +import org.mockito.Mockito.CALLS_REAL_METHODS +import org.mockito.Mockito.mockStatic +import java.net.URL +import java.security.KeyFactory +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.PKCS8EncodedKeySpec +import java.security.spec.X509EncodedKeySpec +import java.time.Instant +import java.util.* + +class Hs2019HttpSignatureVerifierTest { + @Test + fun hs2019の署名を検証できる() { + val privateKey = Base64.getDecoder().decode( + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDH+8IRQdB9vhej" + + "eNQqwxlIutt4Xxjg3eZ5XYm7KgR4jAT/q7+cfsz2/fVTNwDOBpG2H1YJZRLUsrVI" + + "3Pia8k7oFyemYKEUN6kaKioO5C9hB0TaiTGlxAKI7syitvedH1YtHqf4zvqgtI2y" + + "Lv0NAXja3nUoWZWgZg8ZUHWAGhtHd/BpLiyvlwXAydFHl/eN5u8uCZhQD0bKI5KJ" + + "qi8Vcs59Q13+ZNHFdgvNjeD+Hc2AdOmbB5HrW2wREgd1sygBcZ77sSLyleeel8+d" + + "VkDsq/l4MQV5/0PeOn+PI8pJkyQUpqnExqzku354XW9ZE6XjREgBb9M0AEdyBLk5" + + "+YbZwiqrAgMBAAECggEAIDgmKB7xDDvYFcpIbytHo47B/+ldBL2Q0vTdqn3hLTAX" + + "OL80URj3b25dsVknPrToPO4HhTP3jgp3Z+nR/oS+Gb5r8O5DMBKs9+jbJdMK9G2g" + + "tjoW+ZypcTj9VynLSFEy0nTMndVwTlFIkvCRwcqpl07yk9xQXas+ZixZrJiIKeyW" + + "rCmBDJAjUSknljHDnULxAXvk6K7Y5uqPCv9DQ1362ZopY56H0++9ZMaJwr5PYJMT" + + "QoKVeZCGLvfY29rUrhV0/CvC7cfxrPbSuQ7Tr/WrpxFpz9H/Dnc8uePoUEmMP2GM" + + "ozjXaJDQrzOVMOpn/2uGinmrcR5/8ETsYruGG96kAQKBgQDoS4PyiZ93zTv5Yvo/" + + "aWX5IvieMO/w3kRvsdq0IM27Gd+Ck+0C7WBUqljuU4ql10mp67MFkj7ZmECWCAKa" + + "OfE3NtXKqnRgixDhM4Q7nfolhkN08CxRrYP3dBh7HJMDtb2YzPDdwV+PSbL6AM8o" + + "oOvxjABJQk6CaGdmL8sQwnHwgQKBgQDcZCJbwDyLPJo04dXkhPBWoH5hGjbtsAv9" + + "whhjY9IWFN/0KvJfzfoTWtgCkpYT3wgMYBVp0aTextbg1euim7X+iVL6TRq4QaVk" + + "Jv6dRnNZPrY7MlnXxIXE81z6syVjChrJBWi81s14SDKtGOpvghLiKUR29wvGjfER" + + "jY/X1MxFKwKBgAUyOz9fqLuLUb4gYqysdOV/zMPtIFDpB+rftZ615SQ8Te2j1Xdt" + + "S+xY6yhZog5XpIQyi4yiWtmPOFKi1zwP879icKHZ8kR+l+ARwPF8dS4FtNiWzsb8" + + "9KjCZhHK79bzZ8xVOUYcn0CbS2+gOQIVp3F9yjvZSdxM7ZMxmn9Dej0BAoGBAJuf" + + "ZZeOLfJPz8AJvCSKLr+swrDEdwbtqfn8xYXhJacMBHwAm3dFFhH2stNWOP09HwzG" + + "CDjZnWbl1zOaOrJu61saEurF6Vk0mZoX4vChn6/kFX/FdSVkEuVYx04LlBnUN8e8" + + "txGpSBtoN8h88IXevoDOjRbIKZuB/Tjc0jagf8FTAoGAWW8uXsWS4c2nBGNdodqL" + + "xJHcNZVMenHPqdkm7rHEYdf1sdbM7r7Q+oj0cifhbZwaRG9HiRGUAgJerLGEqe+x" + + "vNeYuKRF3A5xBFUTw/t+XFhUZ1sSyvOordp0uNahQqkAx1UQFWUBCEkG2k/X81fY" + + "trEnKP2IjOJDzoXGvc4TG0w=" + ) + + val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(privateKey) + val rsaPrivateKey = KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey + + val publicKey = Base64.getDecoder().decode( + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx/vCEUHQfb4Xo3jUKsMZ" + + "SLrbeF8Y4N3meV2JuyoEeIwE/6u/nH7M9v31UzcAzgaRth9WCWUS1LK1SNz4mvJO" + + "6BcnpmChFDepGioqDuQvYQdE2okxpcQCiO7Morb3nR9WLR6n+M76oLSNsi79DQF4" + + "2t51KFmVoGYPGVB1gBobR3fwaS4sr5cFwMnRR5f3jebvLgmYUA9GyiOSiaovFXLO" + + "fUNd/mTRxXYLzY3g/h3NgHTpmweR61tsERIHdbMoAXGe+7Ei8pXnnpfPnVZA7Kv5" + + "eDEFef9D3jp/jyPKSZMkFKapxMas5Lt+eF1vWROl40RIAW/TNABHcgS5OfmG2cIq" + + "qwIDAQAB" + ) + + val x509EncodedKeySpec = X509EncodedKeySpec(publicKey) + val rsaPublicKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey + + + val hs2019HttpSignatureSigner = Hs2019HttpSignatureSigner(10, 64) + val headers = HttpHeaders( + mapOf( + "X-Request-Id" to listOf("00000000-0000-0000-0000-000000000004"), + "Tpp-Redirect-Uri" to listOf("https://www.sometpp.com/redirect/"), + "Digest" to listOf("SHA-256=TGGHcPGLechhcNo4gndoKUvCBhWaQOPgtoVDIpxc6J4="), + "Psu-Id" to listOf("1337") + ) + ) + val url = URL("https://example.com/") + val httpRequest = HttpRequest(url, headers, HttpMethod.GET) + val signature = hs2019HttpSignatureSigner.sign( + httpRequest, PrivateKey(rsaPrivateKey, "https://test-hideout.usbharu.dev/users/c#pubkey"), + listOf("x-request-id", "tpp-redirect-uri", "digest", "psu-id") + ) + val hs2019HttpSignatureVerifier = Hs2019HttpSignatureVerifier(DefaultSignatureHeaderParser(), 64) + val verify = hs2019HttpSignatureVerifier.verify( + HttpRequest( + url, + headers.plus("Signature", listOf(signature.signatureHeader)), + HttpMethod.GET + ), PublicKey(rsaPublicKey, "https://test-hideout.usbharu.dev/users/c#pubkey") + ) + + if (verify is FailedVerification) { + println(verify.reason) + } + assertInstanceOf(SuccessfulVerification::class.java, verify) + } + + @Test + fun createdが指定されてても署名を検証できる() { + val privateKey = Base64.getDecoder().decode( + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDH+8IRQdB9vhej" + + "eNQqwxlIutt4Xxjg3eZ5XYm7KgR4jAT/q7+cfsz2/fVTNwDOBpG2H1YJZRLUsrVI" + + "3Pia8k7oFyemYKEUN6kaKioO5C9hB0TaiTGlxAKI7syitvedH1YtHqf4zvqgtI2y" + + "Lv0NAXja3nUoWZWgZg8ZUHWAGhtHd/BpLiyvlwXAydFHl/eN5u8uCZhQD0bKI5KJ" + + "qi8Vcs59Q13+ZNHFdgvNjeD+Hc2AdOmbB5HrW2wREgd1sygBcZ77sSLyleeel8+d" + + "VkDsq/l4MQV5/0PeOn+PI8pJkyQUpqnExqzku354XW9ZE6XjREgBb9M0AEdyBLk5" + + "+YbZwiqrAgMBAAECggEAIDgmKB7xDDvYFcpIbytHo47B/+ldBL2Q0vTdqn3hLTAX" + + "OL80URj3b25dsVknPrToPO4HhTP3jgp3Z+nR/oS+Gb5r8O5DMBKs9+jbJdMK9G2g" + + "tjoW+ZypcTj9VynLSFEy0nTMndVwTlFIkvCRwcqpl07yk9xQXas+ZixZrJiIKeyW" + + "rCmBDJAjUSknljHDnULxAXvk6K7Y5uqPCv9DQ1362ZopY56H0++9ZMaJwr5PYJMT" + + "QoKVeZCGLvfY29rUrhV0/CvC7cfxrPbSuQ7Tr/WrpxFpz9H/Dnc8uePoUEmMP2GM" + + "ozjXaJDQrzOVMOpn/2uGinmrcR5/8ETsYruGG96kAQKBgQDoS4PyiZ93zTv5Yvo/" + + "aWX5IvieMO/w3kRvsdq0IM27Gd+Ck+0C7WBUqljuU4ql10mp67MFkj7ZmECWCAKa" + + "OfE3NtXKqnRgixDhM4Q7nfolhkN08CxRrYP3dBh7HJMDtb2YzPDdwV+PSbL6AM8o" + + "oOvxjABJQk6CaGdmL8sQwnHwgQKBgQDcZCJbwDyLPJo04dXkhPBWoH5hGjbtsAv9" + + "whhjY9IWFN/0KvJfzfoTWtgCkpYT3wgMYBVp0aTextbg1euim7X+iVL6TRq4QaVk" + + "Jv6dRnNZPrY7MlnXxIXE81z6syVjChrJBWi81s14SDKtGOpvghLiKUR29wvGjfER" + + "jY/X1MxFKwKBgAUyOz9fqLuLUb4gYqysdOV/zMPtIFDpB+rftZ615SQ8Te2j1Xdt" + + "S+xY6yhZog5XpIQyi4yiWtmPOFKi1zwP879icKHZ8kR+l+ARwPF8dS4FtNiWzsb8" + + "9KjCZhHK79bzZ8xVOUYcn0CbS2+gOQIVp3F9yjvZSdxM7ZMxmn9Dej0BAoGBAJuf" + + "ZZeOLfJPz8AJvCSKLr+swrDEdwbtqfn8xYXhJacMBHwAm3dFFhH2stNWOP09HwzG" + + "CDjZnWbl1zOaOrJu61saEurF6Vk0mZoX4vChn6/kFX/FdSVkEuVYx04LlBnUN8e8" + + "txGpSBtoN8h88IXevoDOjRbIKZuB/Tjc0jagf8FTAoGAWW8uXsWS4c2nBGNdodqL" + + "xJHcNZVMenHPqdkm7rHEYdf1sdbM7r7Q+oj0cifhbZwaRG9HiRGUAgJerLGEqe+x" + + "vNeYuKRF3A5xBFUTw/t+XFhUZ1sSyvOordp0uNahQqkAx1UQFWUBCEkG2k/X81fY" + + "trEnKP2IjOJDzoXGvc4TG0w=" + ) + + val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(privateKey) + val rsaPrivateKey = KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey + + val publicKey = Base64.getDecoder().decode( + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx/vCEUHQfb4Xo3jUKsMZ" + + "SLrbeF8Y4N3meV2JuyoEeIwE/6u/nH7M9v31UzcAzgaRth9WCWUS1LK1SNz4mvJO" + + "6BcnpmChFDepGioqDuQvYQdE2okxpcQCiO7Morb3nR9WLR6n+M76oLSNsi79DQF4" + + "2t51KFmVoGYPGVB1gBobR3fwaS4sr5cFwMnRR5f3jebvLgmYUA9GyiOSiaovFXLO" + + "fUNd/mTRxXYLzY3g/h3NgHTpmweR61tsERIHdbMoAXGe+7Ei8pXnnpfPnVZA7Kv5" + + "eDEFef9D3jp/jyPKSZMkFKapxMas5Lt+eF1vWROl40RIAW/TNABHcgS5OfmG2cIq" + + "qwIDAQAB" + ) + + val x509EncodedKeySpec = X509EncodedKeySpec(publicKey) + val rsaPublicKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey + + + val hs2019HttpSignatureSigner = Hs2019HttpSignatureSigner(10, 64) + val headers = HttpHeaders( + mapOf( + "X-Request-Id" to listOf("00000000-0000-0000-0000-000000000004"), + "Tpp-Redirect-Uri" to listOf("https://www.sometpp.com/redirect/"), + "Digest" to listOf("SHA-256=TGGHcPGLechhcNo4gndoKUvCBhWaQOPgtoVDIpxc6J4="), + "Psu-Id" to listOf("1337") + ) + ) + val url = URL("https://example.com/") + val httpRequest = HttpRequest(url, headers, HttpMethod.GET) + val signature = hs2019HttpSignatureSigner.sign( + httpRequest, PrivateKey(rsaPrivateKey, "https://test-hideout.usbharu.dev/users/c#pubkey"), + listOf("(created)","x-request-id", "tpp-redirect-uri", "digest", "psu-id") + ) + val hs2019HttpSignatureVerifier = Hs2019HttpSignatureVerifier(DefaultSignatureHeaderParser(), 64) + val verify = hs2019HttpSignatureVerifier.verify( + HttpRequest( + url, + headers.plus("Signature", listOf(signature.signatureHeader)), + HttpMethod.GET + ), PublicKey(rsaPublicKey, "https://test-hideout.usbharu.dev/users/c#pubkey") + ) + + if (verify is FailedVerification) { + println(verify.reason) + } + assertInstanceOf(SuccessfulVerification::class.java, verify) + } + + @Test + fun expiresが指定されてても署名を検証できる() { + val privateKey = Base64.getDecoder().decode( + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDH+8IRQdB9vhej" + + "eNQqwxlIutt4Xxjg3eZ5XYm7KgR4jAT/q7+cfsz2/fVTNwDOBpG2H1YJZRLUsrVI" + + "3Pia8k7oFyemYKEUN6kaKioO5C9hB0TaiTGlxAKI7syitvedH1YtHqf4zvqgtI2y" + + "Lv0NAXja3nUoWZWgZg8ZUHWAGhtHd/BpLiyvlwXAydFHl/eN5u8uCZhQD0bKI5KJ" + + "qi8Vcs59Q13+ZNHFdgvNjeD+Hc2AdOmbB5HrW2wREgd1sygBcZ77sSLyleeel8+d" + + "VkDsq/l4MQV5/0PeOn+PI8pJkyQUpqnExqzku354XW9ZE6XjREgBb9M0AEdyBLk5" + + "+YbZwiqrAgMBAAECggEAIDgmKB7xDDvYFcpIbytHo47B/+ldBL2Q0vTdqn3hLTAX" + + "OL80URj3b25dsVknPrToPO4HhTP3jgp3Z+nR/oS+Gb5r8O5DMBKs9+jbJdMK9G2g" + + "tjoW+ZypcTj9VynLSFEy0nTMndVwTlFIkvCRwcqpl07yk9xQXas+ZixZrJiIKeyW" + + "rCmBDJAjUSknljHDnULxAXvk6K7Y5uqPCv9DQ1362ZopY56H0++9ZMaJwr5PYJMT" + + "QoKVeZCGLvfY29rUrhV0/CvC7cfxrPbSuQ7Tr/WrpxFpz9H/Dnc8uePoUEmMP2GM" + + "ozjXaJDQrzOVMOpn/2uGinmrcR5/8ETsYruGG96kAQKBgQDoS4PyiZ93zTv5Yvo/" + + "aWX5IvieMO/w3kRvsdq0IM27Gd+Ck+0C7WBUqljuU4ql10mp67MFkj7ZmECWCAKa" + + "OfE3NtXKqnRgixDhM4Q7nfolhkN08CxRrYP3dBh7HJMDtb2YzPDdwV+PSbL6AM8o" + + "oOvxjABJQk6CaGdmL8sQwnHwgQKBgQDcZCJbwDyLPJo04dXkhPBWoH5hGjbtsAv9" + + "whhjY9IWFN/0KvJfzfoTWtgCkpYT3wgMYBVp0aTextbg1euim7X+iVL6TRq4QaVk" + + "Jv6dRnNZPrY7MlnXxIXE81z6syVjChrJBWi81s14SDKtGOpvghLiKUR29wvGjfER" + + "jY/X1MxFKwKBgAUyOz9fqLuLUb4gYqysdOV/zMPtIFDpB+rftZ615SQ8Te2j1Xdt" + + "S+xY6yhZog5XpIQyi4yiWtmPOFKi1zwP879icKHZ8kR+l+ARwPF8dS4FtNiWzsb8" + + "9KjCZhHK79bzZ8xVOUYcn0CbS2+gOQIVp3F9yjvZSdxM7ZMxmn9Dej0BAoGBAJuf" + + "ZZeOLfJPz8AJvCSKLr+swrDEdwbtqfn8xYXhJacMBHwAm3dFFhH2stNWOP09HwzG" + + "CDjZnWbl1zOaOrJu61saEurF6Vk0mZoX4vChn6/kFX/FdSVkEuVYx04LlBnUN8e8" + + "txGpSBtoN8h88IXevoDOjRbIKZuB/Tjc0jagf8FTAoGAWW8uXsWS4c2nBGNdodqL" + + "xJHcNZVMenHPqdkm7rHEYdf1sdbM7r7Q+oj0cifhbZwaRG9HiRGUAgJerLGEqe+x" + + "vNeYuKRF3A5xBFUTw/t+XFhUZ1sSyvOordp0uNahQqkAx1UQFWUBCEkG2k/X81fY" + + "trEnKP2IjOJDzoXGvc4TG0w=" + ) + + val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(privateKey) + val rsaPrivateKey = KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey + + val publicKey = Base64.getDecoder().decode( + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx/vCEUHQfb4Xo3jUKsMZ" + + "SLrbeF8Y4N3meV2JuyoEeIwE/6u/nH7M9v31UzcAzgaRth9WCWUS1LK1SNz4mvJO" + + "6BcnpmChFDepGioqDuQvYQdE2okxpcQCiO7Morb3nR9WLR6n+M76oLSNsi79DQF4" + + "2t51KFmVoGYPGVB1gBobR3fwaS4sr5cFwMnRR5f3jebvLgmYUA9GyiOSiaovFXLO" + + "fUNd/mTRxXYLzY3g/h3NgHTpmweR61tsERIHdbMoAXGe+7Ei8pXnnpfPnVZA7Kv5" + + "eDEFef9D3jp/jyPKSZMkFKapxMas5Lt+eF1vWROl40RIAW/TNABHcgS5OfmG2cIq" + + "qwIDAQAB" + ) + + val x509EncodedKeySpec = X509EncodedKeySpec(publicKey) + val rsaPublicKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey + + + val hs2019HttpSignatureSigner = Hs2019HttpSignatureSigner(10, 64) + val headers = HttpHeaders( + mapOf( + "X-Request-Id" to listOf("00000000-0000-0000-0000-000000000004"), + "Tpp-Redirect-Uri" to listOf("https://www.sometpp.com/redirect/"), + "Digest" to listOf("SHA-256=TGGHcPGLechhcNo4gndoKUvCBhWaQOPgtoVDIpxc6J4="), + "Psu-Id" to listOf("1337") + ) + ) + val url = URL("https://example.com/") + val httpRequest = HttpRequest(url, headers, HttpMethod.GET) + val signature = hs2019HttpSignatureSigner.sign( + httpRequest, PrivateKey(rsaPrivateKey, "https://test-hideout.usbharu.dev/users/c#pubkey"), + listOf("(expires)","x-request-id", "tpp-redirect-uri", "digest", "psu-id") + ) + val hs2019HttpSignatureVerifier = Hs2019HttpSignatureVerifier(DefaultSignatureHeaderParser(), 64) + val verify = hs2019HttpSignatureVerifier.verify( + HttpRequest( + url, + headers.plus("Signature", listOf(signature.signatureHeader)), + HttpMethod.GET + ), PublicKey(rsaPublicKey, "https://test-hideout.usbharu.dev/users/c#pubkey") + ) + + if (verify is FailedVerification) { + println(verify.reason) + } + assertInstanceOf(SuccessfulVerification::class.java, verify) + } + + @Test + fun createdが未来のとき署名に失敗する() { + val plusSeconds = Instant.now().plusSeconds(100000) + val mockStatic = mockStatic(Instant::class.java, CALLS_REAL_METHODS) + mockStatic.`when`(Instant::now).thenReturn(plusSeconds) + + val privateKey = Base64.getDecoder().decode( + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDH+8IRQdB9vhej" + + "eNQqwxlIutt4Xxjg3eZ5XYm7KgR4jAT/q7+cfsz2/fVTNwDOBpG2H1YJZRLUsrVI" + + "3Pia8k7oFyemYKEUN6kaKioO5C9hB0TaiTGlxAKI7syitvedH1YtHqf4zvqgtI2y" + + "Lv0NAXja3nUoWZWgZg8ZUHWAGhtHd/BpLiyvlwXAydFHl/eN5u8uCZhQD0bKI5KJ" + + "qi8Vcs59Q13+ZNHFdgvNjeD+Hc2AdOmbB5HrW2wREgd1sygBcZ77sSLyleeel8+d" + + "VkDsq/l4MQV5/0PeOn+PI8pJkyQUpqnExqzku354XW9ZE6XjREgBb9M0AEdyBLk5" + + "+YbZwiqrAgMBAAECggEAIDgmKB7xDDvYFcpIbytHo47B/+ldBL2Q0vTdqn3hLTAX" + + "OL80URj3b25dsVknPrToPO4HhTP3jgp3Z+nR/oS+Gb5r8O5DMBKs9+jbJdMK9G2g" + + "tjoW+ZypcTj9VynLSFEy0nTMndVwTlFIkvCRwcqpl07yk9xQXas+ZixZrJiIKeyW" + + "rCmBDJAjUSknljHDnULxAXvk6K7Y5uqPCv9DQ1362ZopY56H0++9ZMaJwr5PYJMT" + + "QoKVeZCGLvfY29rUrhV0/CvC7cfxrPbSuQ7Tr/WrpxFpz9H/Dnc8uePoUEmMP2GM" + + "ozjXaJDQrzOVMOpn/2uGinmrcR5/8ETsYruGG96kAQKBgQDoS4PyiZ93zTv5Yvo/" + + "aWX5IvieMO/w3kRvsdq0IM27Gd+Ck+0C7WBUqljuU4ql10mp67MFkj7ZmECWCAKa" + + "OfE3NtXKqnRgixDhM4Q7nfolhkN08CxRrYP3dBh7HJMDtb2YzPDdwV+PSbL6AM8o" + + "oOvxjABJQk6CaGdmL8sQwnHwgQKBgQDcZCJbwDyLPJo04dXkhPBWoH5hGjbtsAv9" + + "whhjY9IWFN/0KvJfzfoTWtgCkpYT3wgMYBVp0aTextbg1euim7X+iVL6TRq4QaVk" + + "Jv6dRnNZPrY7MlnXxIXE81z6syVjChrJBWi81s14SDKtGOpvghLiKUR29wvGjfER" + + "jY/X1MxFKwKBgAUyOz9fqLuLUb4gYqysdOV/zMPtIFDpB+rftZ615SQ8Te2j1Xdt" + + "S+xY6yhZog5XpIQyi4yiWtmPOFKi1zwP879icKHZ8kR+l+ARwPF8dS4FtNiWzsb8" + + "9KjCZhHK79bzZ8xVOUYcn0CbS2+gOQIVp3F9yjvZSdxM7ZMxmn9Dej0BAoGBAJuf" + + "ZZeOLfJPz8AJvCSKLr+swrDEdwbtqfn8xYXhJacMBHwAm3dFFhH2stNWOP09HwzG" + + "CDjZnWbl1zOaOrJu61saEurF6Vk0mZoX4vChn6/kFX/FdSVkEuVYx04LlBnUN8e8" + + "txGpSBtoN8h88IXevoDOjRbIKZuB/Tjc0jagf8FTAoGAWW8uXsWS4c2nBGNdodqL" + + "xJHcNZVMenHPqdkm7rHEYdf1sdbM7r7Q+oj0cifhbZwaRG9HiRGUAgJerLGEqe+x" + + "vNeYuKRF3A5xBFUTw/t+XFhUZ1sSyvOordp0uNahQqkAx1UQFWUBCEkG2k/X81fY" + + "trEnKP2IjOJDzoXGvc4TG0w=" + ) + + val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(privateKey) + val rsaPrivateKey = KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey + + val publicKey = Base64.getDecoder().decode( + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx/vCEUHQfb4Xo3jUKsMZ" + + "SLrbeF8Y4N3meV2JuyoEeIwE/6u/nH7M9v31UzcAzgaRth9WCWUS1LK1SNz4mvJO" + + "6BcnpmChFDepGioqDuQvYQdE2okxpcQCiO7Morb3nR9WLR6n+M76oLSNsi79DQF4" + + "2t51KFmVoGYPGVB1gBobR3fwaS4sr5cFwMnRR5f3jebvLgmYUA9GyiOSiaovFXLO" + + "fUNd/mTRxXYLzY3g/h3NgHTpmweR61tsERIHdbMoAXGe+7Ei8pXnnpfPnVZA7Kv5" + + "eDEFef9D3jp/jyPKSZMkFKapxMas5Lt+eF1vWROl40RIAW/TNABHcgS5OfmG2cIq" + + "qwIDAQAB" + ) + + val x509EncodedKeySpec = X509EncodedKeySpec(publicKey) + val rsaPublicKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey + + + val hs2019HttpSignatureSigner = Hs2019HttpSignatureSigner(10, 64) + val headers = HttpHeaders( + mapOf( + "X-Request-Id" to listOf("00000000-0000-0000-0000-000000000004"), + "Tpp-Redirect-Uri" to listOf("https://www.sometpp.com/redirect/"), + "Digest" to listOf("SHA-256=TGGHcPGLechhcNo4gndoKUvCBhWaQOPgtoVDIpxc6J4="), + "Psu-Id" to listOf("1337") + ) + ) + val url = URL("https://example.com/") + val httpRequest = HttpRequest(url, headers, HttpMethod.GET) + val signature = hs2019HttpSignatureSigner.sign( + httpRequest, PrivateKey(rsaPrivateKey, "https://test-hideout.usbharu.dev/users/c#pubkey"), + listOf("(created)","x-request-id", "tpp-redirect-uri", "digest", "psu-id") + ) + + mockStatic.close() + val hs2019HttpSignatureVerifier = Hs2019HttpSignatureVerifier(DefaultSignatureHeaderParser(), 64) + val verify = hs2019HttpSignatureVerifier.verify( + HttpRequest( + url, + headers.plus("Signature", listOf(signature.signatureHeader)), + HttpMethod.GET + ), PublicKey(rsaPublicKey, "https://test-hideout.usbharu.dev/users/c#pubkey") + ) + + assertInstanceOf(FailedVerification::class.java, verify) + assertEquals("(created) is the future.",(verify as FailedVerification).reason) + } + + @Test + fun expiresが期限切れのとき失敗する() { + val plusSeconds = Instant.now().minusSeconds(100000) + val mockStatic = mockStatic(Instant::class.java, CALLS_REAL_METHODS) + mockStatic.`when`(Instant::now).thenReturn(plusSeconds) + + val privateKey = Base64.getDecoder().decode( + "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDH+8IRQdB9vhej" + + "eNQqwxlIutt4Xxjg3eZ5XYm7KgR4jAT/q7+cfsz2/fVTNwDOBpG2H1YJZRLUsrVI" + + "3Pia8k7oFyemYKEUN6kaKioO5C9hB0TaiTGlxAKI7syitvedH1YtHqf4zvqgtI2y" + + "Lv0NAXja3nUoWZWgZg8ZUHWAGhtHd/BpLiyvlwXAydFHl/eN5u8uCZhQD0bKI5KJ" + + "qi8Vcs59Q13+ZNHFdgvNjeD+Hc2AdOmbB5HrW2wREgd1sygBcZ77sSLyleeel8+d" + + "VkDsq/l4MQV5/0PeOn+PI8pJkyQUpqnExqzku354XW9ZE6XjREgBb9M0AEdyBLk5" + + "+YbZwiqrAgMBAAECggEAIDgmKB7xDDvYFcpIbytHo47B/+ldBL2Q0vTdqn3hLTAX" + + "OL80URj3b25dsVknPrToPO4HhTP3jgp3Z+nR/oS+Gb5r8O5DMBKs9+jbJdMK9G2g" + + "tjoW+ZypcTj9VynLSFEy0nTMndVwTlFIkvCRwcqpl07yk9xQXas+ZixZrJiIKeyW" + + "rCmBDJAjUSknljHDnULxAXvk6K7Y5uqPCv9DQ1362ZopY56H0++9ZMaJwr5PYJMT" + + "QoKVeZCGLvfY29rUrhV0/CvC7cfxrPbSuQ7Tr/WrpxFpz9H/Dnc8uePoUEmMP2GM" + + "ozjXaJDQrzOVMOpn/2uGinmrcR5/8ETsYruGG96kAQKBgQDoS4PyiZ93zTv5Yvo/" + + "aWX5IvieMO/w3kRvsdq0IM27Gd+Ck+0C7WBUqljuU4ql10mp67MFkj7ZmECWCAKa" + + "OfE3NtXKqnRgixDhM4Q7nfolhkN08CxRrYP3dBh7HJMDtb2YzPDdwV+PSbL6AM8o" + + "oOvxjABJQk6CaGdmL8sQwnHwgQKBgQDcZCJbwDyLPJo04dXkhPBWoH5hGjbtsAv9" + + "whhjY9IWFN/0KvJfzfoTWtgCkpYT3wgMYBVp0aTextbg1euim7X+iVL6TRq4QaVk" + + "Jv6dRnNZPrY7MlnXxIXE81z6syVjChrJBWi81s14SDKtGOpvghLiKUR29wvGjfER" + + "jY/X1MxFKwKBgAUyOz9fqLuLUb4gYqysdOV/zMPtIFDpB+rftZ615SQ8Te2j1Xdt" + + "S+xY6yhZog5XpIQyi4yiWtmPOFKi1zwP879icKHZ8kR+l+ARwPF8dS4FtNiWzsb8" + + "9KjCZhHK79bzZ8xVOUYcn0CbS2+gOQIVp3F9yjvZSdxM7ZMxmn9Dej0BAoGBAJuf" + + "ZZeOLfJPz8AJvCSKLr+swrDEdwbtqfn8xYXhJacMBHwAm3dFFhH2stNWOP09HwzG" + + "CDjZnWbl1zOaOrJu61saEurF6Vk0mZoX4vChn6/kFX/FdSVkEuVYx04LlBnUN8e8" + + "txGpSBtoN8h88IXevoDOjRbIKZuB/Tjc0jagf8FTAoGAWW8uXsWS4c2nBGNdodqL" + + "xJHcNZVMenHPqdkm7rHEYdf1sdbM7r7Q+oj0cifhbZwaRG9HiRGUAgJerLGEqe+x" + + "vNeYuKRF3A5xBFUTw/t+XFhUZ1sSyvOordp0uNahQqkAx1UQFWUBCEkG2k/X81fY" + + "trEnKP2IjOJDzoXGvc4TG0w=" + ) + + val pkcS8EncodedKeySpec = PKCS8EncodedKeySpec(privateKey) + val rsaPrivateKey = KeyFactory.getInstance("RSA").generatePrivate(pkcS8EncodedKeySpec) as RSAPrivateKey + + val publicKey = Base64.getDecoder().decode( + "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx/vCEUHQfb4Xo3jUKsMZ" + + "SLrbeF8Y4N3meV2JuyoEeIwE/6u/nH7M9v31UzcAzgaRth9WCWUS1LK1SNz4mvJO" + + "6BcnpmChFDepGioqDuQvYQdE2okxpcQCiO7Morb3nR9WLR6n+M76oLSNsi79DQF4" + + "2t51KFmVoGYPGVB1gBobR3fwaS4sr5cFwMnRR5f3jebvLgmYUA9GyiOSiaovFXLO" + + "fUNd/mTRxXYLzY3g/h3NgHTpmweR61tsERIHdbMoAXGe+7Ei8pXnnpfPnVZA7Kv5" + + "eDEFef9D3jp/jyPKSZMkFKapxMas5Lt+eF1vWROl40RIAW/TNABHcgS5OfmG2cIq" + + "qwIDAQAB" + ) + + val x509EncodedKeySpec = X509EncodedKeySpec(publicKey) + val rsaPublicKey = KeyFactory.getInstance("RSA").generatePublic(x509EncodedKeySpec) as RSAPublicKey + + + val hs2019HttpSignatureSigner = Hs2019HttpSignatureSigner(10, 64) + val headers = HttpHeaders( + mapOf( + "X-Request-Id" to listOf("00000000-0000-0000-0000-000000000004"), + "Tpp-Redirect-Uri" to listOf("https://www.sometpp.com/redirect/"), + "Digest" to listOf("SHA-256=TGGHcPGLechhcNo4gndoKUvCBhWaQOPgtoVDIpxc6J4="), + "Psu-Id" to listOf("1337") + ) + ) + val url = URL("https://example.com/") + val httpRequest = HttpRequest(url, headers, HttpMethod.GET) + val signature = hs2019HttpSignatureSigner.sign( + httpRequest, PrivateKey(rsaPrivateKey, "https://test-hideout.usbharu.dev/users/c#pubkey"), + listOf("(expires)","x-request-id", "tpp-redirect-uri", "digest", "psu-id") + ) + + mockStatic.close() + val hs2019HttpSignatureVerifier = Hs2019HttpSignatureVerifier(DefaultSignatureHeaderParser(), 64) + val verify = hs2019HttpSignatureVerifier.verify( + HttpRequest( + url, + headers.plus("Signature", listOf(signature.signatureHeader)), + HttpMethod.GET + ), PublicKey(rsaPublicKey, "https://test-hideout.usbharu.dev/users/c#pubkey") + ) + + assertInstanceOf(FailedVerification::class.java, verify) + assertEquals("(expires) is expired.",(verify as FailedVerification).reason) + } +} diff --git a/src/test/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifierImplTest.kt b/src/test/kotlin/dev/usbharu/httpsignature/verify/RsaSha256HttpSignatureVerifierTest.kt similarity index 95% rename from src/test/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifierImplTest.kt rename to src/test/kotlin/dev/usbharu/httpsignature/verify/RsaSha256HttpSignatureVerifierTest.kt index 21644da..998de4d 100644 --- a/src/test/kotlin/dev/usbharu/httpsignature/verify/HttpSignatureVerifierImplTest.kt +++ b/src/test/kotlin/dev/usbharu/httpsignature/verify/RsaSha256HttpSignatureVerifierTest.kt @@ -12,7 +12,7 @@ import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec import java.util.* -class HttpSignatureVerifierImplTest { +class RsaSha256HttpSignatureVerifierTest { @Test fun 署名の検証を行える() { val privateKey = Base64.getDecoder().decode( @@ -76,9 +76,9 @@ class HttpSignatureVerifierImplTest { listOf("x-request-id", "tpp-redirect-uri", "digest", "psu-id") ) - val httpSignatureVerifierImpl = - HttpSignatureVerifierImpl(DefaultSignatureHeaderParser(), rsaSha256HttpSignatureSigner) - val verify = httpSignatureVerifierImpl.verify( + val rsaSha256HttpSignatureVerifier = + RsaSha256HttpSignatureVerifier(DefaultSignatureHeaderParser(), rsaSha256HttpSignatureSigner) + val verify = rsaSha256HttpSignatureVerifier.verify( HttpRequest(url, headers.plus("Signature", listOf(signature.signatureHeader)), HttpMethod.GET), PublicKey(rsaPublicKey, "https://test-hideout.usbharu.dev/users/c#pubkey") )