Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support x5u signed jwt verification #24

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class OpenIdProvider(
val clientScheme = payload.clientIdScheme ?: authorizationRequestPayload.clientIdScheme

if (clientScheme == "x509_san_dns") {
val verifyResult = JWT.verifyJwtByX5C(requestObjectJwt)
val verifyResult = JWT.verifyJwtWithX509Certs(requestObjectJwt)
if (!verifyResult.isSuccess) {
return Result.failure(Exception("Invalid request"))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,13 @@ fun decodeUriAsJson(uri: String): Map<String, Any> {
val mapper = jacksonObjectMapper()

for (param in params) {
if (param.size != 2) continue
if (param.size < 2) continue
val key = URLDecoder.decode(param[0], "UTF-8")
val value = URLDecoder.decode(param[1], "UTF-8")
var value = URLDecoder.decode(param[1], "UTF-8")
if (param.size == 3) {
value += "="
value += URLDecoder.decode(param[2], "UTF-8")
}

when {
value.toBooleanStrictOrNull() != null -> json[key] = value.toBoolean()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,57 @@ class JWT {
}
}

private fun getX509Certs(jwt: String): 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")
if (certs.isMissing) {
val url = decodedJwt.getHeaderClaim("x5u")
if (url != null) {
try {
return SignatureUtil.getX509CertificatesFromUrl(url.asString())
} catch (e: Exception) {
println(e)
return null
}
}
} else {
return convertPemToX509Certificates(certs.asList(String::class.java))
}
return null
}

fun verifyJwtWithX509Certs(jwt: String): Result<Pair<DecodedJWT, Array<X509Certificate>>> {
val certificates = getX509Certs(jwt)
if (certificates.isNullOrEmpty()) {
return Result.failure(Exception("Certificate list could not be retrieved"))
}
try {
val result = verifyJwt(jwt, certificates[0].publicKey)
val isTestEnvironment =
System.getProperty("isTestEnvironment")?.toBoolean() ?: false
val b = if (isTestEnvironment) {
validateCertificateChain(certificates, certificates.last())
} else {
validateCertificateChain(certificates)
}
// todo row to der エンコーディングの変換ができずjava.security.Signatureを使った実装が未対応(ES256Kサポートのためには対応が必要)
return if (result.isRight() && b) {
val decoded = result.getOrNull()!!
Result.success(Pair(decoded, certificates))
} else {
Result.failure(Exception("Digital signature verification failed"))
}
} catch (e: Exception) {
println(e)
return Result.failure(Exception("Digital signature verification failed"))
}
}

@Deprecated(
"This function is deprecated, use verifyJwtWithX509Certs(jwt: String) instead",
)
fun verifyJwtByX5C(jwt: String): Result<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
Expand Down Expand Up @@ -153,6 +204,9 @@ class JWT {
}
}

@Deprecated(
"This function is deprecated, use verifyJwtWithX509Certs(jwt: String) instead",
)
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
@@ -1,6 +1,7 @@
package com.ownd_project.tw2023_wallet_android

import com.auth0.jwt.JWT
import com.auth0.jwt.JWTCreator
import com.auth0.jwt.algorithms.Algorithm
import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil
import com.ownd_project.tw2023_wallet_android.utils.generateEcKeyPair
Expand All @@ -9,6 +10,7 @@ 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 com.ownd_project.tw2023_wallet_android.signature.JWT.Companion.verifyJwtWithX509Certs
import org.junit.After
import org.junit.Assert
import org.junit.Before
Expand Down Expand Up @@ -72,31 +74,22 @@ class CredentialVerifierTest {
Assert.assertTrue(b)
}

@Test
fun testVerifyJwtByX5U() {
val cert0 = SignatureUtil.generateCertificate(keyPairTestIssuer, keyPairTestCA, false)
private fun getPemChain(): String {
val cert0 = SignatureUtil.generateCertificate(
keyPairTestIssuer,
keyPairTestCA,
false,
listOf("alt1.verifier.com")
)
val cert1 =
SignatureUtil.generateCertificate(keyPairTestCA, keyPairTestCA, true) // 認証局は自己証明
SignatureUtil.generateCertificate(keyPairTestCA, keyPairTestCA, true)
val pem0 = SignatureUtil.certificateToPem(cert0)
val pem1 = SignatureUtil.certificateToPem(cert1)
val pemChain = "$pem0\n$pem1"
wireMockServer.stubFor(
WireMock.get(WireMock.urlEqualTo("/test-certificate"))
.willReturn(
WireMock.aResponse()
.withStatus(200)
.withBody(pemChain)
.withHeader("Content-Type", "application/x-pem-file")
)
)
val x5uUrl = "http://localhost:${wireMockServer.port()}/test-certificate"
return "$pem0\n$pem1"
}

val algorithm =
Algorithm.ECDSA256(
keyPairTestIssuer.public as ECPublicKey,
keyPairTestIssuer.private as ECPrivateKey?
)
val token = JWT.create()
private fun getTestTokenBuilder(): JWTCreator.Builder {
return JWT.create()
.withIssuer("https://university.example/issuers/565049")
.withKeyId("http://university.example/credentials/3732")
.withSubject("did:example:ebfeb1f712ebc6f1c276e12ec21")
Expand All @@ -118,6 +111,92 @@ class CredentialVerifierTest {
)
)
.withIssuedAt(Date())
}

@Test
fun testVerifyJwtWithX509Certs1() {
val pemChain = getPemChain()
wireMockServer.stubFor(
WireMock.get(WireMock.urlEqualTo("/test-certificate"))
.willReturn(
WireMock.aResponse()
.withStatus(200)
.withBody(pemChain)
.withHeader("Content-Type", "application/x-pem-file")
)
)
val x5uUrl = "http://localhost:${wireMockServer.port()}/test-certificate"

val algorithm =
Algorithm.ECDSA256(
keyPairTestIssuer.public as ECPublicKey,
keyPairTestIssuer.private as ECPrivateKey?
)
val token = getTestTokenBuilder()
.withHeader(mapOf("x5u" to x5uUrl))
.sign(algorithm)
val result = verifyJwtWithX509Certs(token)
Assert.assertTrue(result.isSuccess)
val (decodedJwt, certificates) = result.getOrThrow()
if (!certificates[0].hasSubjectAlternativeName("alt1.verifier.com")) {
Assert.fail()
}
val vc = decodedJwt.getClaim("vc")
Assert.assertNotNull(vc)
}

@Test
fun testVerifyJwtWithX509Certs2() {
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 = getTestTokenBuilder()
.withHeader(mapOf("x5c" to certs))
.sign(algorithm)
val result = verifyJwtWithX509Certs(token)
Assert.assertTrue(result.isSuccess)
val (decodedJwt, certificates) = result.getOrThrow()
if (!certificates[0].hasSubjectAlternativeName("alt1.verifier.com")) {
Assert.fail()
}
val vc = decodedJwt.getClaim("vc")
Assert.assertNotNull(vc)
}

@Test
fun testVerifyJwtByX5U() {
val pemChain = getPemChain()
wireMockServer.stubFor(
WireMock.get(WireMock.urlEqualTo("/test-certificate"))
.willReturn(
WireMock.aResponse()
.withStatus(200)
.withBody(pemChain)
.withHeader("Content-Type", "application/x-pem-file")
)
)
val x5uUrl = "http://localhost:${wireMockServer.port()}/test-certificate"

val algorithm =
Algorithm.ECDSA256(
keyPairTestIssuer.public as ECPublicKey,
keyPairTestIssuer.private as ECPrivateKey?
)
val token = getTestTokenBuilder()
.withHeader(mapOf("x5u" to x5uUrl))
.sign(algorithm)
val result = verifyJwtByX5U(token)
Expand All @@ -132,9 +211,15 @@ class CredentialVerifierTest {
}
)
}

@Test
fun testVerifyJwtByX5C() {
val cert0 = SignatureUtil.generateCertificate(keyPairTestIssuer, keyPairTestCA, false, listOf("alt1.verifier.com"))
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) // 認証局は自己証明
Expand All @@ -146,28 +231,7 @@ class CredentialVerifierTest {
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())
val token = getTestTokenBuilder()
.withHeader(mapOf("x5c" to certs))
.sign(algorithm)
val result = verifyJwtByX5C(token)
Expand Down
Loading