From 1e31a1b5e6352d19c6a46d5d91fa0b2e9fef715c Mon Sep 17 00:00:00 2001 From: ryousuke Date: Tue, 11 Jun 2024 16:59:04 +0900 Subject: [PATCH] Support `x_509_san_dns` client_id_scheme value --- .../oid/OpenIdProvider.kt | 42 +++++++++---- .../tw2023_wallet_android/signature/JWT.kt | 34 ++++++++--- .../signature/SignatureUtil.kt | 28 ++++++--- .../CredentialVerifierTest.kt | 59 ++++++++++++++++++- 4 files changed, 135 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/OpenIdProvider.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/OpenIdProvider.kt index affbfaa..93b0d9b 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/OpenIdProvider.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/OpenIdProvider.kt @@ -21,6 +21,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import java.net.URI import java.security.KeyPair +import java.security.cert.X509Certificate import java.util.Base64 import java.util.UUID @@ -127,25 +128,40 @@ class OpenIdProvider(val uri: String, val option: SigningOption = SigningOption( val payloadJson = String(Base64.getUrlDecoder().decode(decodedJwt.payload)) val payload = objectMapper.readValue(payloadJson, RequestObjectPayloadImpl::class.java) + val clientId = payload.clientId?: authorizationRequestPayload.clientId + if (clientId.isNullOrBlank()) { + return Either.Left("Invalid client_id or response_uri") + } val clientScheme = payload.clientIdScheme?: authorizationRequestPayload.clientIdScheme return if (requestIsSigned) { val jwtValidationResult = if (clientScheme == "x509_san_dns") { - JWT.verifyJwtX509SanDns(requestObjectJwt) + val verifyResult = JWT.verifyJwtByX5C(requestObjectJwt) + verifyResult.fold( + ifLeft = { + // throw RuntimeException(it) + Either.Left("Invalid request") + }, + ifRight = {(decodedJwt, certificates) -> + // https://openid.net/specs/openid-4-verifiable-presentations-1_0.html + /* + the Client Identifier MUST be a DNS name and match a dNSName Subject Alternative Name (SAN) [RFC5280] entry in the leaf certificate passed with the request. + */ + if (certificates[0].hasSubjectAlternativeName(clientId)) { + // throw RuntimeException("Invalid client_id or response_uri") + Either.Left("Invalid client_id or response_uri") + } + decodedJwt + } + ) } else { val jwksUrl = registrationMetadata.jwksUri ?: throw IllegalStateException("JWKS URLが見つかりません。") JWT.verifyJwtWithJwks(requestObjectJwt, jwksUrl) } val result = try { - // JWTを検証 - val payloadJson = String(Base64.getUrlDecoder().decode(jwtValidationResult.payload)) - val payload = objectMapper.readValue(payloadJson, RequestObjectPayloadImpl::class.java) - - val clientScheme = payload.clientIdScheme?: authorizationRequestPayload.clientIdScheme if (clientScheme == "redirect_uri") { - val clientId = payload.clientId?: authorizationRequestPayload.clientId val responseUri = payload.responseUri?: authorizationRequestPayload.responseUri if (clientId.isNullOrBlank() || responseUri.isNullOrBlank() || clientId != responseUri) { return Either.Left("Invalid client_id or response_uri") @@ -170,11 +186,6 @@ class OpenIdProvider(val uri: String, val option: SigningOption = SigningOption( } return result } else { - val decodedJwt = com.auth0.jwt.JWT.decode(requestObjectJwt) - val payloadJson = String(Base64.getUrlDecoder().decode(decodedJwt.payload)) - val payload = objectMapper.readValue(payloadJson, RequestObjectPayloadImpl::class.java) - - val clientScheme = payload.clientIdScheme?: authorizationRequestPayload.clientIdScheme if (clientScheme == "redirect_uri") { val clientId = payload.clientId?: authorizationRequestPayload.clientId val responseUri = payload.responseUri?: authorizationRequestPayload.responseUri @@ -590,4 +601,9 @@ data class PostResult( val statusCode: Int, val location: String?, val cookies: Array -) \ No newline at end of file +) + +fun X509Certificate.hasSubjectAlternativeName(target: String): Boolean { + val altNames = this.subjectAlternativeNames ?: return false + return altNames.any { it[1] == target } +} \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/signature/JWT.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/signature/JWT.kt index d1fc214..25bb958 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/signature/JWT.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/signature/JWT.kt @@ -10,6 +10,7 @@ import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil.validateCe import com.ownd_project.tw2023_wallet_android.utils.Constants import com.ownd_project.tw2023_wallet_android.utils.KeyPairUtil import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil.convertPemToX509Certificates import org.jose4j.jwk.HttpsJwks import org.jose4j.jwk.Use import org.jose4j.jws.AlgorithmIdentifiers @@ -18,6 +19,7 @@ import java.nio.charset.StandardCharsets import java.security.PrivateKey import java.security.PublicKey import java.security.Signature +import java.security.cert.X509Certificate import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPublicKey import java.util.Base64 @@ -74,13 +76,6 @@ class JWT { throw JWTVerificationException((result as Either.Left).value) } } - suspend fun verifyJwtX509SanDns(jwt: String): DecodedJWT { - val decodedJwt = JWT.decode(jwt) -// TODO("extract certs") -// TODO("verify jwt") -// TODO("verify certs") - return decodedJwt - } fun verifyJwt(jwt: String, publicKey: PublicKey): Either { // todo 戻り値の型がauto0のライブラリの型で良いか検討する val decodedJwt = JWT.decode(jwt) @@ -124,6 +119,31 @@ class JWT { } } + fun verifyJwtByX5C(jwt: String): Either>> { + // https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.6 + // https://www.rfc-editor.org/rfc/rfc7515.html#appendix-B + val decodedJwt = JWT.decode(jwt) + val certs = decodedJwt.getHeaderClaim("x5c").asList(String::class.java) + try { + val certificates = convertPemToX509Certificates(certs) + + if (certificates.isNullOrEmpty()) { + return Either.Left("証明書リストが取得できませんでした") + } + val result = verifyJwt(jwt, certificates[0].publicKey) + val b = validateCertificateChain(certificates, certificates.last()) + // todo row to der エンコーディングの変換ができずjava.security.Signatureを使った実装が未対応(ES256Kサポートのためには対応が必要) + return if (result.isRight() && b) { + Either.Right(Pair(decodedJwt, certificates)) + } else { + Either.Left("JWTの検証に失敗しました") + } + } catch (e: IOException) { + println(e) + return Either.Left("JWTの検証に失敗しました") + } + } + fun verifyJwtByX5U(jwt: String): Either { val decodedJwt = JWT.decode(jwt) val url = decodedJwt.getHeaderClaim("x5u").asString() diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/signature/SignatureUtil.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/signature/SignatureUtil.kt index 4bcb00a..882d5c5 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/signature/SignatureUtil.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/signature/SignatureUtil.kt @@ -9,6 +9,9 @@ import org.bouncycastle.asn1.DEROctetString import org.bouncycastle.asn1.DERSequenceGenerator import org.bouncycastle.asn1.x509.BasicConstraints import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.Extension.subjectAlternativeName +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNames import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.jce.ECNamedCurveTable @@ -190,7 +193,8 @@ object SignatureUtil { fun generateCertificate( keyPair: KeyPair, signerKeyPair: KeyPair, - isCa: Boolean + isCa: Boolean, + subjectAlternativeNames: List = emptyList() ): X509Certificate { val now = Date() val notBefore = Date(now.time) @@ -216,6 +220,14 @@ object SignatureUtil { ) } + if (subjectAlternativeNames.isNotEmpty()) { + val generalNames = subjectAlternativeNames.map { GeneralName(GeneralName.dNSName, it) } + val subjectAltName = GeneralNames(generalNames.toTypedArray()) + certBuilder.addExtension( + subjectAlternativeName, false, subjectAltName + ) + } + // Signing the certificate using the private key val signer = JcaContentSignerBuilder("SHA256withECDSA").build(signerKeyPair.private) val holder = certBuilder.build(signer) @@ -268,6 +280,7 @@ object SignatureUtil { } fun getX509CertificatesFromUrl(url: String): Array? { + // https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.5 val client = OkHttpClient() val request = Request.Builder().url(url).build() @@ -275,18 +288,19 @@ object SignatureUtil { if (!response.isSuccessful) throw IOException("Failed to download file: $response") val responseBody = response.body()?.string() ?: return null - return convertPemToX509Certificates(responseBody) + return convertPemWithDelimitersToX509Certificates(responseBody) } } - - private fun convertPemToX509Certificates(pem: String): Array? { - val certificateFactory = CertificateFactory.getInstance("X.509") - val certificates = mutableListOf() - + fun convertPemWithDelimitersToX509Certificates(pem: String): Array? { val pemCertificates = pem.trim().split("-----END CERTIFICATE-----") .filter { it.contains("-----BEGIN CERTIFICATE-----") } .map { it.trim() + "\n-----END CERTIFICATE-----" } + return convertPemToX509Certificates(pemCertificates) + } + fun convertPemToX509Certificates(pemCertificates: List): Array? { + val certificateFactory = CertificateFactory.getInstance("X.509") + val certificates = mutableListOf() pemCertificates.forEach { certPem -> val certBytes = Base64.getMimeDecoder().decode( certPem.lineSequence() diff --git a/app/src/test/java/com/ownd_project/tw2023_wallet_android/CredentialVerifierTest.kt b/app/src/test/java/com/ownd_project/tw2023_wallet_android/CredentialVerifierTest.kt index 42ee24e..0fff574 100644 --- a/app/src/test/java/com/ownd_project/tw2023_wallet_android/CredentialVerifierTest.kt +++ b/app/src/test/java/com/ownd_project/tw2023_wallet_android/CredentialVerifierTest.kt @@ -6,12 +6,16 @@ import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil import com.ownd_project.tw2023_wallet_android.utils.generateEcKeyPair import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.WireMock +import com.ownd_project.tw2023_wallet_android.oid.hasSubjectAlternativeName +import com.ownd_project.tw2023_wallet_android.signature.JWT.Companion.verifyJwtByX5C +import com.ownd_project.tw2023_wallet_android.signature.JWT.Companion.verifyJwtByX5U import org.junit.After import org.junit.Assert import org.junit.Before import org.junit.Test import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey +import java.util.Base64 import java.util.Date class CredentialVerifierTest { @@ -112,7 +116,7 @@ class CredentialVerifierTest { .withIssuedAt(Date()) .withHeader(mapOf("x5u" to x5uUrl)) .sign(algorithm) - val result = com.ownd_project.tw2023_wallet_android.signature.JWT.verifyJwtByX5U(token) + val result = verifyJwtByX5U(token) Assert.assertTrue(result.isRight()) result.fold( ifLeft = { @@ -124,5 +128,58 @@ class CredentialVerifierTest { } ) } + @Test + fun testVerifyJwtByX5C() { + val cert0 = SignatureUtil.generateCertificate(keyPairTestIssuer, keyPairTestCA, false, listOf("alt1.verifier.com")) + val encodedCert0 = Base64.getEncoder().encodeToString(cert0.encoded) + val cert1 = + SignatureUtil.generateCertificate(keyPairTestCA, keyPairTestCA, true) // 認証局は自己証明 + val encodedCert1 = Base64.getEncoder().encodeToString(cert1.encoded) + val certs = listOf(encodedCert0, encodedCert1) + + val algorithm = + Algorithm.ECDSA256( + keyPairTestIssuer.public as ECPublicKey, + keyPairTestIssuer.private as ECPrivateKey? + ) + val token = JWT.create() + .withIssuer("https://university.example/issuers/565049") + .withKeyId("http://university.example/credentials/3732") + .withSubject("did:example:ebfeb1f712ebc6f1c276e12ec21") + .withClaim( + "vc", mapOf( + "@context" to listOf( + "https://www.w3.org/ns/credentials/v2", + "https://www.w3.org/ns/credentials/examples/v2" + ), + "id" to "http://university.example/credentials/3732", + "type" to listOf("VerifiableCredential", "ExampleDegreeCredential"), + "issuer" to "https://university.example/issuers/565049", + "validFrom" to "2010-01-01T00:00:00Z", + "credentialSubject" to mapOf( + "id" to "did:example:ebfeb1f712ebc6f1c276e12ec21", + "name" to "Sample Event ABC", + "date" to "2024-01-24T00:00:00Z", + ) + ) + ) + .withIssuedAt(Date()) + .withHeader(mapOf("x5c" to certs)) + .sign(algorithm) + val result = verifyJwtByX5C(token) + Assert.assertTrue(result.isRight()) + result.fold( + ifLeft = { + Assert.fail() + }, + ifRight = {(decodedJwt, certificates) -> + if (!certificates[0].hasSubjectAlternativeName("alt1.verifier.com")) { + Assert.fail() + } + val vc = decodedJwt.getClaim("vc") + Assert.assertNotNull(vc) + } + ) + } }