Skip to content

Commit

Permalink
Support x_509_san_dns client_id_scheme value
Browse files Browse the repository at this point in the history
  • Loading branch information
ryosuke-wakaba committed Jun 11, 2024
1 parent 6e4642c commit 1e31a1b
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand Down Expand Up @@ -590,4 +601,9 @@ data class PostResult(
val statusCode: Int,
val location: String?,
val cookies: Array<String>
)
)

fun X509Certificate.hasSubjectAlternativeName(target: String): Boolean {
val altNames = this.subjectAlternativeNames ?: return false
return altNames.any { it[1] == target }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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<String, DecodedJWT> { // todo 戻り値の型がauto0のライブラリの型で良いか検討する
val decodedJwt = JWT.decode(jwt)
Expand Down Expand Up @@ -124,6 +119,31 @@ class JWT {
}
}

fun verifyJwtByX5C(jwt: String): Either<String, Pair<DecodedJWT, Array<X509Certificate>>> {
// 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<String, DecodedJWT> {
val decodedJwt = JWT.decode(jwt)
val url = decodedJwt.getHeaderClaim("x5u").asString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -190,7 +193,8 @@ object SignatureUtil {
fun generateCertificate(
keyPair: KeyPair,
signerKeyPair: KeyPair,
isCa: Boolean
isCa: Boolean,
subjectAlternativeNames: List<String> = emptyList()
): X509Certificate {
val now = Date()
val notBefore = Date(now.time)
Expand All @@ -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)
Expand Down Expand Up @@ -268,25 +280,27 @@ object SignatureUtil {
}

fun getX509CertificatesFromUrl(url: String): Array<X509Certificate>? {
// https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.5
val client = OkHttpClient()
val request = Request.Builder().url(url).build()

client.newCall(request).execute().use { response ->
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<X509Certificate>? {
val certificateFactory = CertificateFactory.getInstance("X.509")
val certificates = mutableListOf<X509Certificate>()

fun convertPemWithDelimitersToX509Certificates(pem: String): Array<X509Certificate>? {
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<String>): Array<X509Certificate>? {
val certificateFactory = CertificateFactory.getInstance("X.509")
val certificates = mutableListOf<X509Certificate>()
pemCertificates.forEach { certPem ->
val certBytes = Base64.getMimeDecoder().decode(
certPem.lineSequence()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = {
Expand All @@ -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)
}
)
}
}

0 comments on commit 1e31a1b

Please sign in to comment.