From 8ae9746facb29d8ac01c0fddc08a6010c62ad964 Mon Sep 17 00:00:00 2001 From: ryousuke Date: Mon, 3 Jun 2024 18:58:43 +0900 Subject: [PATCH 1/7] Include credentials in `jwt_vc_json` format within `jwt_vp_json` VP in the response --- .../JwtVpJsonGeneratorTest.kt | 53 +++++++++ .../oid/OpenIdProvider.kt | 107 +++++------------ .../tw2023_wallet_android/oid/Presentation.kt | 108 ++++++++++++++++++ .../tw2023_wallet_android/oid/URL.kt | 11 +- .../tw2023_wallet_android/signature/JWT.kt | 7 ++ .../signature/SignatureUtil.kt | 67 +++++++++++ .../ui/shared/Constants.kt | 1 + .../ui/shared/JwtVpJsonGeneratorImpl.kt | 57 +++++++++ .../ui/siop_vp/TokenSharingViewModel.kt | 6 +- 9 files changed, 334 insertions(+), 83 deletions(-) create mode 100644 app/src/androidTest/java/com/ownd_project/tw2023_wallet_android/JwtVpJsonGeneratorTest.kt create mode 100644 app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/Presentation.kt create mode 100644 app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt diff --git a/app/src/androidTest/java/com/ownd_project/tw2023_wallet_android/JwtVpJsonGeneratorTest.kt b/app/src/androidTest/java/com/ownd_project/tw2023_wallet_android/JwtVpJsonGeneratorTest.kt new file mode 100644 index 0000000..8bb528d --- /dev/null +++ b/app/src/androidTest/java/com/ownd_project/tw2023_wallet_android/JwtVpJsonGeneratorTest.kt @@ -0,0 +1,53 @@ +package com.ownd_project.tw2023_wallet_android + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.ownd_project.tw2023_wallet_android.oid.HeaderOptions +import com.ownd_project.tw2023_wallet_android.oid.JwtVpJsonPayloadOptions +import com.ownd_project.tw2023_wallet_android.ui.shared.JwtVpJsonGeneratorImpl +import com.ownd_project.tw2023_wallet_android.utils.KeyPairUtil +import junit.framework.TestCase +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class JwtVpJsonGeneratorTest { + + private lateinit var jwtVpJsonGenerator: JwtVpJsonGeneratorImpl + private val keyAlias = "testKeyAlias" + + @Before + fun setUp() { + jwtVpJsonGenerator = JwtVpJsonGeneratorImpl(keyAlias) + if (!KeyPairUtil.isKeyPairExist(keyAlias)) { + KeyPairUtil.generateSignVerifyKeyPair(keyAlias) + } + } + + @Test + fun testGenerateJwt() { + val vcJwt = "testVcJwt" + val headerOptions = HeaderOptions() + val payloadOptions = JwtVpJsonPayloadOptions( + iss = "issuer", + jti = "testJti", + aud = "testAud", + nonce = "testNonce" + ) + + val vpToken = jwtVpJsonGenerator.generateJwt(vcJwt, headerOptions, payloadOptions) + + // 生成されたJWTを検証するためのロジックを追加してください。 + // ここでは例として、JWTが非空であることを確認しています。 + assert(!vpToken.isEmpty()) + + val decodedJwt = KeyPairUtil.decodeJwt(vpToken) + val header = decodedJwt.first + + val jwk = header.get("jwk") as Map + val result = KeyPairUtil.verifyJwt(jwk, vpToken) + TestCase.assertTrue(result) + } +} 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 2e1c849..b13ad7f 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 @@ -14,32 +14,23 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.convertValue import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.ownd_project.tw2023_wallet_android.signature.toBase64Url +import com.ownd_project.tw2023_wallet_android.signature.ProviderOption +import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request import org.bouncycastle.jce.spec.ECNamedCurveSpec -import java.math.BigInteger import java.net.URI -import java.net.URLEncoder import java.security.KeyPair -import java.security.PublicKey import java.security.interfaces.ECPublicKey -import java.security.interfaces.RSAPublicKey import java.security.spec.ECParameterSpec -import java.security.spec.ECPoint import java.util.Base64 import java.util.UUID -data class ProviderOption( - val expiresIn: Int = 600, - val signingAlgo: String = "ES256K", - val signingCurve: String = "P-256", -) - -class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOption()) { +class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOption(signingAlgo = "ES256K")) { private lateinit var keyPair: KeyPair private lateinit var keyBinding: KeyBinding + private lateinit var jwtVpJsonGenerator: JwtVpJsonGenerator private lateinit var siopRequest: ProcessSIOPRequestResult companion object { @@ -107,6 +98,9 @@ class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOptio fun setKeyBinding(keyBinding: KeyBinding) { this.keyBinding = keyBinding } + fun setJwtVpJsonGenerator(jwtVpJsonGenerator: JwtVpJsonGenerator) { + this.jwtVpJsonGenerator = jwtVpJsonGenerator + } fun getSiopRequest(): ProcessSIOPRequestResult { return this.siopRequest @@ -131,14 +125,23 @@ class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOptio } registerModule(module) } + 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 return if (requestIsSigned) { - val jwksUrl = - registrationMetadata.jwksUri ?: throw IllegalStateException("JWKS URLが見つかりません。") + val jwtValidationResult = + if (clientScheme == "x509_san_dns") { + JWT.verifyJwtX509SanDns(requestObjectJwt) + } else { + val jwksUrl = registrationMetadata.jwksUri ?: throw IllegalStateException("JWKS URLが見つかりません。") + JWT.verifyJwtWithJwks(requestObjectJwt, jwksUrl) + } val result = try { // JWTを検証 - val jwtValidationResult = JWT.verifyJwtWithJwks(requestObjectJwt, jwksUrl) val payloadJson = String(Base64.getUrlDecoder().decode(jwtValidationResult.payload)) val payload = objectMapper.readValue(payloadJson, RequestObjectPayloadImpl::class.java) @@ -204,7 +207,7 @@ class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOptio val nonce = authRequest.nonce val SEC_IN_MS = 1000 - val subJwk = generatePublicKeyJwk(keyPair, option) + val subJwk = SignatureUtil.generatePublicKeyJwk(keyPair, option) // todo: support rsa key val jwk = object : ECPublicJwk { override val kty = subJwk["kty"]!! @@ -399,78 +402,24 @@ class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOptio val disclosedClaims = payload.mapNotNull { (key, _) -> DisclosedClaim(id = credential.id, types = credential.types, name = key) } - val pathNested = Path(format = credential.format, path = "$") - val dm = DescriptorMap( - id = credential.inputDescriptor.id, - format = credential.format, - path = "$", - pathNested = pathNested + val vpToken = this.jwtVpJsonGenerator.generateJwt( + credential.credential, + HeaderOptions(), + JwtVpJsonPayloadOptions(aud = authRequest.clientId!!, nonce = authRequest.nonce!!) ) - // todo check credential is matched condition specified by input_descriptor + return Triple( - first = credential.credential, - second = dm, + first = vpToken, + second = JwtVpJsonPresentation.genDescriptorMap(presentationDefinition.inputDescriptors[0].id), third = disclosedClaims ) + } catch (error: Exception) { throw error } } } -fun generatePublicKeyJwk(keyPair: KeyPair, option: ProviderOption): Map { - val publicKey: PublicKey = keyPair.public - - return when (publicKey) { - is RSAPublicKey -> generateRsaPublicKeyJwk(publicKey) - is ECPublicKey -> generateEcPublicKeyJwk(publicKey, option) - else -> throw IllegalArgumentException("Unsupported Key Type: ${publicKey::class.java.name}") - } -} - -fun correctBytes(value: BigInteger): ByteArray { - /* - BigInteger の toByteArray() メソッドは、数値をバイト配列に変換しますが、 - この数値が正の場合、最上位バイトが符号ビットとして解釈されることを避けるために、追加のゼロバイトが先頭に挿入されることがあります。 - これは、数値が正で、最上位バイトが 0x80 以上の場合(つまり、最上位ビットが 1 の場合)に起こります。 - その結果、期待していた 32 バイトではなく 33 バイトの配列が得られることがあります。 - - 期待する 32 バイトの配列を得るには、返されたバイト配列から余分なゼロバイトを取り除くか、 - または正確なバイト長を指定して配列を生成する必要があります。 - */ - val bytes = value.toByteArray() - return if (bytes.size == 33 && bytes[0] == 0.toByte()) bytes.copyOfRange( - 1, - bytes.size - ) else bytes -} - -fun generateEcPublicKeyJwk(ecPublicKey: ECPublicKey, option: ProviderOption): Map { - val ecPoint: ECPoint = ecPublicKey.w - val x = correctBytes(ecPoint.affineX).toBase64Url() - val y = correctBytes(ecPoint.affineY).toBase64Url() - - // return """{"kty":"EC","crv":"P-256","x":"$x","y":"$y"}""" // crvは適宜変更してください - return mapOf( - "kty" to "EC", - "crv" to option.signingCurve, - "x" to x, - "y" to y - ) -} - -fun generateRsaPublicKeyJwk(rsaPublicKey: RSAPublicKey): Map { - val n = Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()) - val e = Base64.getUrlEncoder().encodeToString(rsaPublicKey.publicExponent.toByteArray()) - - // return """{"kty":"RSA","n":"$n","e":"$e"}""" - return mapOf( - "kty" to "RSA", - "n" to n, - "e" to e - ) -} - fun getCurveName(ecPublicKey: ECPublicKey): String { val params = ecPublicKey.params diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/Presentation.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/Presentation.kt new file mode 100644 index 0000000..39bb2b9 --- /dev/null +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/Presentation.kt @@ -0,0 +1,108 @@ +package com.ownd_project.tw2023_wallet_android.oid + +import com.fasterxml.jackson.annotation.JsonInclude + +@JsonInclude(JsonInclude.Include.NON_NULL) +data class VpJwtPayload( + val iss: String?, + val jti: String?, + val aud: String?, + val nbf: Long?, + val iat: Long?, + val exp: Long?, + val nonce: String?, + val vp: Map +) +// https://www.rfc-editor.org/rfc/rfc7515.html +data class HeaderOptions( + val alg: String = "ES256", + val typ: String = "JWT", + val jwk: String? = null +) + +data class JwtVpJsonPayloadOptions( + val iss: String? = null, + val jti: String? = null, + val aud: String, + val nbf: Long? = null, + val iat: Long? = null, + val exp: Long? = null, + val nonce: String +) + +object JwtVpJsonPresentation { + fun genDescriptorMap( + inputDescriptorId: String, + pathIndex: Int? = -1, + pathNestedIndex: Int? = 0 + ): DescriptorMap { + + /* + a non-normative example of the content of the presentation_submission parameter: + ``` + { + "definition_id": "example_jwt_vc", + "id": "example_jwt_vc_presentation_submission", + "descriptor_map": [ + { + "id": "id_credential", + "path": "$", + "format": "jwt_vp_json", + "path_nested": { + "path": "$.vp.verifiableCredential[0]", + "format": "jwt_vc_json" + } + } + ] + } + ``` + */ + return DescriptorMap( + id = inputDescriptorId, + path = if (pathIndex == -1) "$" else "$[${pathIndex}]", + format = "jwt_vp_json", + pathNested = Path( + format = "jwt_vc_json", + path = "$.vp.verifiableCredential[${pathNestedIndex}]" + ) + ) + } +} + +// https://openid.net/specs/openid-4-verifiable-presentations-1_0-20.html#name-presentation-response +interface JwtVpJsonGenerator { + /* + a non-normative example of the payload of the Verifiable Presentation in the vp_token parameter + ``` + { + "iss": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "jti": "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5", + "aud": "https://client.example.org/cb", + "nbf": 1541493724, + "iat": 1541493724, + "exp": 1573029723, + "nonce": "n-0S6_WzA2Mj", + "vp": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + "eyJhb...ssw5c" + ] + } + } + ``` + Note: The VP's nonce claim contains the value of the nonce of the presentation request and the aud claim contains the Client Identifier of the Verifier. + This allows the Verifier to detect replay of a Presentation as recommended in Section 12.1. + */ + fun generateJwt( + vcJwt: String, + headerOptions: HeaderOptions, + payloadOptions: JwtVpJsonPayloadOptions + ): String + + fun getJwk(): Map +} \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/URL.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/URL.kt index 151fabd..90c46dc 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/URL.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/URL.kt @@ -1,6 +1,9 @@ package com.ownd_project.tw2023_wallet_android.oid import com.auth0.jwt.JWT +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import kotlinx.coroutines.Dispatchers @@ -73,11 +76,13 @@ suspend fun parseAndResolve(uri: String): ParseAndResolveResult { println("request jwt: $requestObjectJwt") println("get client metadata") - val mapper = jacksonObjectMapper() val decodedJwt = JWT.decode(requestObjectJwt) val clientMetadata = decodedJwt.getClaim("client_metadata")?.let { - val json = mapper.writeValueAsString(it.asString()) - mapper.readValue(json) + val mapper: ObjectMapper = jacksonObjectMapper().apply { + propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE + configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true) + } + mapper.readValue(it.toString()) } println("client metadata: $clientMetadata") val clientMetadataUri = decodedJwt.getClaim("client_metadata_uri").asString()?: authorizationRequestPayload.clientMetadataUri 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 aefcb56..d1fc214 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 @@ -74,6 +74,13 @@ 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) 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 5c2a8fa..ba34361 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 @@ -22,11 +22,14 @@ import java.security.KeyFactory import java.security.KeyPair import java.security.KeyStore import java.security.MessageDigest +import java.security.PublicKey import java.security.Security import java.security.cert.CertPathValidator import java.security.cert.CertificateFactory import java.security.cert.PKIXParameters import java.security.cert.X509Certificate +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey import java.security.spec.ECFieldFp import java.security.spec.ECParameterSpec import java.security.spec.ECPoint @@ -367,6 +370,64 @@ object SignatureUtil { // return false // } + + fun correctBytes(value: BigInteger): ByteArray { + /* + BigInteger の toByteArray() メソッドは、数値をバイト配列に変換しますが、 + この数値が正の場合、最上位バイトが符号ビットとして解釈されることを避けるために、追加のゼロバイトが先頭に挿入されることがあります。 + これは、数値が正で、最上位バイトが 0x80 以上の場合(つまり、最上位ビットが 1 の場合)に起こります。 + その結果、期待していた 32 バイトではなく 33 バイトの配列が得られることがあります。 + + 期待する 32 バイトの配列を得るには、返されたバイト配列から余分なゼロバイトを取り除くか、 + または正確なバイト長を指定して配列を生成する必要があります。 + */ + val bytes = value.toByteArray() + return if (bytes.size == 33 && bytes[0] == 0.toByte()) bytes.copyOfRange( + 1, + bytes.size + ) else bytes + } + + fun generateEcPublicKeyJwk( + ecPublicKey: ECPublicKey, + option: ProviderOption + ): Map { + val ecPoint: ECPoint = ecPublicKey.w + val x = correctBytes(ecPoint.affineX).toBase64Url() + val y = correctBytes(ecPoint.affineY).toBase64Url() + + // return """{"kty":"EC","crv":"P-256","x":"$x","y":"$y"}""" // crvは適宜変更してください + return mapOf( + "kty" to "EC", + "crv" to option.signingCurve, + "x" to x, + "y" to y + ) + } + + fun generateRsaPublicKeyJwk(rsaPublicKey: RSAPublicKey): Map { + val n = Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()) + val e = Base64.getUrlEncoder().encodeToString(rsaPublicKey.publicExponent.toByteArray()) + + // return """{"kty":"RSA","n":"$n","e":"$e"}""" + return mapOf( + "kty" to "RSA", + "n" to n, + "e" to e + ) + } + + fun generatePublicKeyJwk(keyPair: KeyPair, option: ProviderOption): Map { + val publicKey: PublicKey = keyPair.public + return generatePublicKeyJwk(publicKey, option) + } + fun generatePublicKeyJwk(publicKey: PublicKey, option: ProviderOption): Map { + return when (publicKey) { + is RSAPublicKey -> generateRsaPublicKeyJwk(publicKey) + is ECPublicKey -> generateEcPublicKeyJwk(publicKey, option) + else -> throw IllegalArgumentException("Unsupported Key Type: ${publicKey::class.java.name}") + } + } fun byte2Base64Url(b: ByteArray) = b.toBase64Url() fun int2Base64Url(i: BigInteger) = i.toBase64Url() @@ -375,3 +436,9 @@ object SignatureUtil { fun ByteArray.toBase64Url() = Base64.getUrlEncoder().encodeToString(this).trimEnd('=') fun BigInteger.toBase64Url() = Base64.getUrlEncoder().encodeToString(this.toByteArray()).trimEnd('=') + +data class ProviderOption( + val expiresIn: Int = 600, + val signingAlgo: String = "ES256", + val signingCurve: String = "P-256", +) \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/Constants.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/Constants.kt index 286aac4..28e15c7 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/Constants.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/Constants.kt @@ -2,4 +2,5 @@ package com.ownd_project.tw2023_wallet_android.ui.shared object Constants { const val KEY_PAIR_ALIAS_FOR_KEY_BINDING = "bindingKey"; + const val KEY_PAIR_ALIAS_FOR_KEY_JWT_VP_JSON = "jwtVpJsonKey"; } \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt new file mode 100644 index 0000000..1baff7c --- /dev/null +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt @@ -0,0 +1,57 @@ +package com.ownd_project.tw2023_wallet_android.ui.shared + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.ownd_project.tw2023_wallet_android.oid.HeaderOptions +import com.ownd_project.tw2023_wallet_android.oid.VpJwtPayload +import com.ownd_project.tw2023_wallet_android.oid.JwtVpJsonGenerator +import com.ownd_project.tw2023_wallet_android.oid.JwtVpJsonPayloadOptions +import com.ownd_project.tw2023_wallet_android.signature.JWT +import com.ownd_project.tw2023_wallet_android.signature.ProviderOption +import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil +import com.ownd_project.tw2023_wallet_android.utils.KeyPairUtil +import java.security.PublicKey + +class JwtVpJsonGeneratorImpl(private val keyAlias: String = Constants.KEY_PAIR_ALIAS_FOR_KEY_JWT_VP_JSON) : + JwtVpJsonGenerator { + override fun generateJwt( + vcJwt: String, + headerOptions: HeaderOptions, + payloadOptions: JwtVpJsonPayloadOptions + ): String { + val vpClaims = mapOf( + "@context" to listOf("https://www.w3.org/2018/credentials/v1"), + "type" to listOf("VerifiablePresentation"), + "verifiableCredential" to listOf(vcJwt) + ) + + val currentTimeSeconds = System.currentTimeMillis() / 1000 + val header = + mapOf("alg" to headerOptions.alg, "typ" to headerOptions.typ, "jwk" to getJwk()) + val jwtPayload = VpJwtPayload( + iss = payloadOptions.iss, + jti = payloadOptions.jti, + aud = payloadOptions.aud, + nbf = payloadOptions.nbf ?: currentTimeSeconds, + iat = payloadOptions.iat ?: currentTimeSeconds, + exp = payloadOptions.exp ?: (currentTimeSeconds + 2 * 3600), + nonce = payloadOptions.nonce, + vp = vpClaims + ) + val objectMapper = jacksonObjectMapper() + val vpTokenPayload = + objectMapper.convertValue(jwtPayload, Map::class.java) as Map + return JWT.sign(keyAlias, header, vpTokenPayload) + } + + override fun getJwk(): Map { + if (!KeyPairUtil.isKeyPairExist(keyAlias)) { + KeyPairUtil.generateSignVerifyKeyPair(keyAlias) + } + val publicKey: PublicKey = KeyPairUtil.getPublicKey(keyAlias) + ?: throw IllegalStateException("Public key not found for alias: $keyAlias") + val jwk = SignatureUtil.generatePublicKeyJwk(publicKey, ProviderOption()) + return jwk +// val objectMapper = jacksonObjectMapper() +// return objectMapper.writeValueAsString(jwk) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/siop_vp/TokenSharingViewModel.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/siop_vp/TokenSharingViewModel.kt index 2840df8..05b546c 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/siop_vp/TokenSharingViewModel.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/siop_vp/TokenSharingViewModel.kt @@ -18,7 +18,7 @@ import com.ownd_project.tw2023_wallet_android.datastore.PreferencesDataStore import com.ownd_project.tw2023_wallet_android.pairwise.HDKeyRing import com.ownd_project.tw2023_wallet_android.oid.OpenIdProvider import com.ownd_project.tw2023_wallet_android.oid.PresentationDefinition -import com.ownd_project.tw2023_wallet_android.oid.ProviderOption +import com.ownd_project.tw2023_wallet_android.signature.ProviderOption import com.ownd_project.tw2023_wallet_android.oid.SubmissionCredential import com.ownd_project.tw2023_wallet_android.signature.ECPrivateJwk import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil @@ -28,6 +28,7 @@ import com.ownd_project.tw2023_wallet_android.utils.CertificateInfo import com.ownd_project.tw2023_wallet_android.utils.CertificateUtil.getCertificateInformation import com.google.protobuf.Timestamp import com.ownd_project.tw2023_wallet_android.oid.PostResult +import com.ownd_project.tw2023_wallet_android.ui.shared.JwtVpJsonGeneratorImpl import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -198,6 +199,9 @@ class IdTokenSharringViewModel : ViewModel() { } else { val keyBinding = KeyBindingImpl(Constants.KEY_PAIR_ALIAS_FOR_KEY_BINDING) openIdProvider.setKeyBinding(keyBinding) + + val jwtVpJsonGenerator = JwtVpJsonGeneratorImpl(Constants.KEY_PAIR_ALIAS_FOR_KEY_JWT_VP_JSON) + openIdProvider.setJwtVpJsonGenerator(jwtVpJsonGenerator) } // getServerCertificates("https://datasign.jp/") From 11e8506b80c88cdd4147e3308fd53d7a8b3a0e1d Mon Sep 17 00:00:00 2001 From: ryousuke Date: Tue, 4 Jun 2024 14:26:18 +0900 Subject: [PATCH 2/7] refactor --- .../JwtVpJsonGeneratorTest.kt | 2 - .../oid/OpenIdProvider.kt | 39 +++--------- .../signature/SignatureUtil.kt | 57 ----------------- .../ui/shared/JwtVpJsonGeneratorImpl.kt | 4 +- .../utils/KeyPairUtil.kt | 61 +++++++++++++++++++ 5 files changed, 69 insertions(+), 94 deletions(-) diff --git a/app/src/androidTest/java/com/ownd_project/tw2023_wallet_android/JwtVpJsonGeneratorTest.kt b/app/src/androidTest/java/com/ownd_project/tw2023_wallet_android/JwtVpJsonGeneratorTest.kt index 8bb528d..7d814f1 100644 --- a/app/src/androidTest/java/com/ownd_project/tw2023_wallet_android/JwtVpJsonGeneratorTest.kt +++ b/app/src/androidTest/java/com/ownd_project/tw2023_wallet_android/JwtVpJsonGeneratorTest.kt @@ -39,8 +39,6 @@ class JwtVpJsonGeneratorTest { val vpToken = jwtVpJsonGenerator.generateJwt(vcJwt, headerOptions, payloadOptions) - // 生成されたJWTを検証するためのロジックを追加してください。 - // ここでは例として、JWTが非空であることを確認しています。 assert(!vpToken.isEmpty()) val decodedJwt = KeyPairUtil.decodeJwt(vpToken) 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 b13ad7f..69b56c1 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 @@ -15,18 +15,16 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.convertValue import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.ownd_project.tw2023_wallet_android.signature.ProviderOption -import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil +import com.ownd_project.tw2023_wallet_android.utils.KeyPairUtil import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request -import org.bouncycastle.jce.spec.ECNamedCurveSpec import java.net.URI import java.security.KeyPair -import java.security.interfaces.ECPublicKey -import java.security.spec.ECParameterSpec import java.util.Base64 import java.util.UUID + class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOption(signingAlgo = "ES256K")) { private lateinit var keyPair: KeyPair private lateinit var keyBinding: KeyBinding @@ -207,7 +205,7 @@ class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOptio val nonce = authRequest.nonce val SEC_IN_MS = 1000 - val subJwk = SignatureUtil.generatePublicKeyJwk(keyPair, option) + val subJwk = KeyPairUtil.generatePublicKeyJwk(keyPair, option) // todo: support rsa key val jwk = object : ECPublicJwk { override val kty = subJwk["kty"]!! @@ -405,7 +403,10 @@ class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOptio val vpToken = this.jwtVpJsonGenerator.generateJwt( credential.credential, HeaderOptions(), - JwtVpJsonPayloadOptions(aud = authRequest.clientId!!, nonce = authRequest.nonce!!) + JwtVpJsonPayloadOptions( + aud = authRequest.clientId!!, + nonce = authRequest.nonce!! + ) ) return Triple( @@ -420,32 +421,6 @@ class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOptio } } -fun getCurveName(ecPublicKey: ECPublicKey): String { - val params = ecPublicKey.params - - return when (params) { - is ECNamedCurveSpec -> { - // Bouncy Castle の ECNamedCurveSpec の場合 - params.name - } - - is ECParameterSpec -> { - val curve = params.curve - // 標準の Java ECParameterSpec の場合 - // ここでは、標準の Java API では曲線名を直接取得できないため、 - // 曲線のオーダーのビット長などに基づいて推定する方法を採用する - when (params.order.bitLength()) { - 256 -> "P-256" - 384 -> "P-384" - 521 -> "P-521" - else -> "不明なカーブ" - } - } - - else -> "サポートされていないパラメータタイプ" - } -} - fun mergeOAuth2AndOpenIdInRequestPayload( payload: AuthorizationRequestPayload, requestObject: RequestObjectPayload? = null 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 ba34361..aef4af7 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 @@ -371,63 +371,6 @@ object SignatureUtil { // } - fun correctBytes(value: BigInteger): ByteArray { - /* - BigInteger の toByteArray() メソッドは、数値をバイト配列に変換しますが、 - この数値が正の場合、最上位バイトが符号ビットとして解釈されることを避けるために、追加のゼロバイトが先頭に挿入されることがあります。 - これは、数値が正で、最上位バイトが 0x80 以上の場合(つまり、最上位ビットが 1 の場合)に起こります。 - その結果、期待していた 32 バイトではなく 33 バイトの配列が得られることがあります。 - - 期待する 32 バイトの配列を得るには、返されたバイト配列から余分なゼロバイトを取り除くか、 - または正確なバイト長を指定して配列を生成する必要があります。 - */ - val bytes = value.toByteArray() - return if (bytes.size == 33 && bytes[0] == 0.toByte()) bytes.copyOfRange( - 1, - bytes.size - ) else bytes - } - - fun generateEcPublicKeyJwk( - ecPublicKey: ECPublicKey, - option: ProviderOption - ): Map { - val ecPoint: ECPoint = ecPublicKey.w - val x = correctBytes(ecPoint.affineX).toBase64Url() - val y = correctBytes(ecPoint.affineY).toBase64Url() - - // return """{"kty":"EC","crv":"P-256","x":"$x","y":"$y"}""" // crvは適宜変更してください - return mapOf( - "kty" to "EC", - "crv" to option.signingCurve, - "x" to x, - "y" to y - ) - } - - fun generateRsaPublicKeyJwk(rsaPublicKey: RSAPublicKey): Map { - val n = Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()) - val e = Base64.getUrlEncoder().encodeToString(rsaPublicKey.publicExponent.toByteArray()) - - // return """{"kty":"RSA","n":"$n","e":"$e"}""" - return mapOf( - "kty" to "RSA", - "n" to n, - "e" to e - ) - } - - fun generatePublicKeyJwk(keyPair: KeyPair, option: ProviderOption): Map { - val publicKey: PublicKey = keyPair.public - return generatePublicKeyJwk(publicKey, option) - } - fun generatePublicKeyJwk(publicKey: PublicKey, option: ProviderOption): Map { - return when (publicKey) { - is RSAPublicKey -> generateRsaPublicKeyJwk(publicKey) - is ECPublicKey -> generateEcPublicKeyJwk(publicKey, option) - else -> throw IllegalArgumentException("Unsupported Key Type: ${publicKey::class.java.name}") - } - } fun byte2Base64Url(b: ByteArray) = b.toBase64Url() fun int2Base64Url(i: BigInteger) = i.toBase64Url() diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt index 1baff7c..455eab9 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt @@ -49,9 +49,7 @@ class JwtVpJsonGeneratorImpl(private val keyAlias: String = Constants.KEY_PAIR_A } val publicKey: PublicKey = KeyPairUtil.getPublicKey(keyAlias) ?: throw IllegalStateException("Public key not found for alias: $keyAlias") - val jwk = SignatureUtil.generatePublicKeyJwk(publicKey, ProviderOption()) + val jwk = KeyPairUtil.generatePublicKeyJwk(publicKey, ProviderOption()) return jwk -// val objectMapper = jacksonObjectMapper() -// return objectMapper.writeValueAsString(jwk) } } \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt index 18b9a29..2985a0b 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt @@ -4,6 +4,8 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import com.ownd_project.tw2023_wallet_android.signature.JWT import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.ownd_project.tw2023_wallet_android.signature.ProviderOption +import com.ownd_project.tw2023_wallet_android.signature.toBase64Url import org.jose4j.jwk.EllipticCurveJsonWebKey import org.jose4j.jwk.JsonWebKey import org.jose4j.lang.JoseException @@ -16,7 +18,9 @@ import java.security.PrivateKey import java.security.PublicKey import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey import java.security.spec.ECGenParameterSpec +import java.security.spec.ECPoint import java.security.spec.X509EncodedKeySpec import java.util.Base64 import javax.crypto.KeyGenerator @@ -135,6 +139,63 @@ object KeyPairUtil { return keyFactory.generatePublic(keySpec) } + fun correctBytes(value: BigInteger): ByteArray { + /* + BigInteger の toByteArray() メソッドは、数値をバイト配列に変換しますが、 + この数値が正の場合、最上位バイトが符号ビットとして解釈されることを避けるために、追加のゼロバイトが先頭に挿入されることがあります。 + これは、数値が正で、最上位バイトが 0x80 以上の場合(つまり、最上位ビットが 1 の場合)に起こります。 + その結果、期待していた 32 バイトではなく 33 バイトの配列が得られることがあります。 + + 期待する 32 バイトの配列を得るには、返されたバイト配列から余分なゼロバイトを取り除くか、 + または正確なバイト長を指定して配列を生成する必要があります。 + */ + val bytes = value.toByteArray() + return if (bytes.size == 33 && bytes[0] == 0.toByte()) bytes.copyOfRange( + 1, + bytes.size + ) else bytes + } + + fun generateEcPublicKeyJwk( + ecPublicKey: ECPublicKey, + option: ProviderOption + ): Map { + val ecPoint: ECPoint = ecPublicKey.w + val x = correctBytes(ecPoint.affineX).toBase64Url() + val y = correctBytes(ecPoint.affineY).toBase64Url() + + // return """{"kty":"EC","crv":"P-256","x":"$x","y":"$y"}""" // crvは適宜変更してください + return mapOf( + "kty" to "EC", + "crv" to option.signingCurve, + "x" to x, + "y" to y + ) + } + + fun generateRsaPublicKeyJwk(rsaPublicKey: RSAPublicKey): Map { + val n = Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()) + val e = Base64.getUrlEncoder().encodeToString(rsaPublicKey.publicExponent.toByteArray()) + + // return """{"kty":"RSA","n":"$n","e":"$e"}""" + return mapOf( + "kty" to "RSA", + "n" to n, + "e" to e + ) + } + + fun generatePublicKeyJwk(keyPair: KeyPair, option: ProviderOption): Map { + val publicKey: PublicKey = keyPair.public + return generatePublicKeyJwk(publicKey, option) + } + fun generatePublicKeyJwk(publicKey: PublicKey, option: ProviderOption): Map { + return when (publicKey) { + is RSAPublicKey -> generateRsaPublicKeyJwk(publicKey) + is ECPublicKey -> generateEcPublicKeyJwk(publicKey, option) + else -> throw IllegalArgumentException("Unsupported Key Type: ${publicKey::class.java.name}") + } + } fun verifyJwt(jwkJson: Map, jwt: String): Boolean { val publicKey = createPublicKey(jwkJson) From fc00d986ac4379d9654b235c974ea6664be10a90 Mon Sep 17 00:00:00 2001 From: ryousuke Date: Tue, 4 Jun 2024 14:48:19 +0900 Subject: [PATCH 3/7] move ProviderOption as SigningOption --- .../tw2023_wallet_android/oid/OpenIdProvider.kt | 6 +++--- .../tw2023_wallet_android/signature/SignatureUtil.kt | 8 -------- .../ui/shared/JwtVpJsonGeneratorImpl.kt | 5 ++--- .../ui/siop_vp/TokenSharingViewModel.kt | 4 ++-- .../tw2023_wallet_android/utils/KeyPairUtil.kt | 12 ++++++++---- 5 files changed, 15 insertions(+), 20 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 69b56c1..6258509 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 @@ -14,7 +14,7 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.convertValue import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.ownd_project.tw2023_wallet_android.signature.ProviderOption +import com.ownd_project.tw2023_wallet_android.utils.SigningOption import com.ownd_project.tw2023_wallet_android.utils.KeyPairUtil import okhttp3.FormBody import okhttp3.OkHttpClient @@ -25,7 +25,7 @@ import java.util.Base64 import java.util.UUID -class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOption(signingAlgo = "ES256K")) { +class OpenIdProvider(val uri: String, val option: SigningOption = SigningOption(signingAlgo = "ES256K")) { private lateinit var keyPair: KeyPair private lateinit var keyBinding: KeyBinding private lateinit var jwtVpJsonGenerator: JwtVpJsonGenerator @@ -222,7 +222,7 @@ class OpenIdProvider(val uri: String, val option: ProviderOption = ProviderOptio iss = sub, aud = Audience.Single(authRequest.clientId!!), iat = (System.currentTimeMillis() / SEC_IN_MS).toLong(), - exp = (System.currentTimeMillis() / SEC_IN_MS + option.expiresIn).toLong(), + exp = (System.currentTimeMillis() / SEC_IN_MS + 600).toLong(), sub = sub, nonce = nonce as? String, subJwk = subJwk, 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 aef4af7..4bcb00a 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 @@ -22,14 +22,11 @@ import java.security.KeyFactory import java.security.KeyPair import java.security.KeyStore import java.security.MessageDigest -import java.security.PublicKey import java.security.Security import java.security.cert.CertPathValidator import java.security.cert.CertificateFactory import java.security.cert.PKIXParameters import java.security.cert.X509Certificate -import java.security.interfaces.ECPublicKey -import java.security.interfaces.RSAPublicKey import java.security.spec.ECFieldFp import java.security.spec.ECParameterSpec import java.security.spec.ECPoint @@ -380,8 +377,3 @@ fun ByteArray.toBase64Url() = Base64.getUrlEncoder().encodeToString(this).trimEn fun BigInteger.toBase64Url() = Base64.getUrlEncoder().encodeToString(this.toByteArray()).trimEnd('=') -data class ProviderOption( - val expiresIn: Int = 600, - val signingAlgo: String = "ES256", - val signingCurve: String = "P-256", -) \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt index 455eab9..f653af1 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt @@ -6,8 +6,7 @@ import com.ownd_project.tw2023_wallet_android.oid.VpJwtPayload import com.ownd_project.tw2023_wallet_android.oid.JwtVpJsonGenerator import com.ownd_project.tw2023_wallet_android.oid.JwtVpJsonPayloadOptions import com.ownd_project.tw2023_wallet_android.signature.JWT -import com.ownd_project.tw2023_wallet_android.signature.ProviderOption -import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil +import com.ownd_project.tw2023_wallet_android.utils.SigningOption import com.ownd_project.tw2023_wallet_android.utils.KeyPairUtil import java.security.PublicKey @@ -49,7 +48,7 @@ class JwtVpJsonGeneratorImpl(private val keyAlias: String = Constants.KEY_PAIR_A } val publicKey: PublicKey = KeyPairUtil.getPublicKey(keyAlias) ?: throw IllegalStateException("Public key not found for alias: $keyAlias") - val jwk = KeyPairUtil.generatePublicKeyJwk(publicKey, ProviderOption()) + val jwk = KeyPairUtil.generatePublicKeyJwk(publicKey, SigningOption()) return jwk } } \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/siop_vp/TokenSharingViewModel.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/siop_vp/TokenSharingViewModel.kt index 05b546c..6b7b64e 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/siop_vp/TokenSharingViewModel.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/siop_vp/TokenSharingViewModel.kt @@ -18,7 +18,7 @@ import com.ownd_project.tw2023_wallet_android.datastore.PreferencesDataStore import com.ownd_project.tw2023_wallet_android.pairwise.HDKeyRing import com.ownd_project.tw2023_wallet_android.oid.OpenIdProvider import com.ownd_project.tw2023_wallet_android.oid.PresentationDefinition -import com.ownd_project.tw2023_wallet_android.signature.ProviderOption +import com.ownd_project.tw2023_wallet_android.utils.SigningOption import com.ownd_project.tw2023_wallet_android.oid.SubmissionCredential import com.ownd_project.tw2023_wallet_android.signature.ECPrivateJwk import com.ownd_project.tw2023_wallet_android.signature.SignatureUtil @@ -162,7 +162,7 @@ class IdTokenSharringViewModel : ViewModel() { private fun processSiopRequest(context: Context, url: String, seed: String, index: Int) { Log.d(TAG, "processSiopRequest") viewModelScope.launch(Dispatchers.IO) { - val opt = ProviderOption(signingCurve = "secp256k1", signingAlgo = "ES256K") + val opt = SigningOption(signingCurve = "secp256k1", signingAlgo = "ES256K") openIdProvider = OpenIdProvider(url, opt) val result = openIdProvider.processAuthorizationRequest() result.fold( diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt index 2985a0b..46b83ff 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt @@ -4,7 +4,6 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import com.ownd_project.tw2023_wallet_android.signature.JWT import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper -import com.ownd_project.tw2023_wallet_android.signature.ProviderOption import com.ownd_project.tw2023_wallet_android.signature.toBase64Url import org.jose4j.jwk.EllipticCurveJsonWebKey import org.jose4j.jwk.JsonWebKey @@ -158,7 +157,7 @@ object KeyPairUtil { fun generateEcPublicKeyJwk( ecPublicKey: ECPublicKey, - option: ProviderOption + option: SigningOption ): Map { val ecPoint: ECPoint = ecPublicKey.w val x = correctBytes(ecPoint.affineX).toBase64Url() @@ -185,11 +184,11 @@ object KeyPairUtil { ) } - fun generatePublicKeyJwk(keyPair: KeyPair, option: ProviderOption): Map { + fun generatePublicKeyJwk(keyPair: KeyPair, option: SigningOption): Map { val publicKey: PublicKey = keyPair.public return generatePublicKeyJwk(publicKey, option) } - fun generatePublicKeyJwk(publicKey: PublicKey, option: ProviderOption): Map { + fun generatePublicKeyJwk(publicKey: PublicKey, option: SigningOption): Map { return when (publicKey) { is RSAPublicKey -> generateRsaPublicKeyJwk(publicKey) is ECPublicKey -> generateEcPublicKeyJwk(publicKey, option) @@ -275,3 +274,8 @@ fun privateKeyToJwk(privateKey: ECPrivateKey, publicKey: ECPublicKey): Map Date: Tue, 4 Jun 2024 15:27:17 +0900 Subject: [PATCH 4/7] rename method name --- .../tw2023_wallet_android/oid/OpenIdProvider.kt | 2 +- .../ui/shared/JwtVpJsonGeneratorImpl.kt | 2 +- .../tw2023_wallet_android/utils/KeyPairUtil.kt | 16 +++++++++------- 3 files changed, 11 insertions(+), 9 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 6258509..c24d3ed 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 @@ -205,7 +205,7 @@ class OpenIdProvider(val uri: String, val option: SigningOption = SigningOption( val nonce = authRequest.nonce val SEC_IN_MS = 1000 - val subJwk = KeyPairUtil.generatePublicKeyJwk(keyPair, option) + val subJwk = KeyPairUtil.keyPairToPublicJwk(keyPair, option) // todo: support rsa key val jwk = object : ECPublicJwk { override val kty = subJwk["kty"]!! diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt index f653af1..104c52c 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt @@ -48,7 +48,7 @@ class JwtVpJsonGeneratorImpl(private val keyAlias: String = Constants.KEY_PAIR_A } val publicKey: PublicKey = KeyPairUtil.getPublicKey(keyAlias) ?: throw IllegalStateException("Public key not found for alias: $keyAlias") - val jwk = KeyPairUtil.generatePublicKeyJwk(publicKey, SigningOption()) + val jwk = KeyPairUtil.publicKeyToJwk(publicKey, SigningOption()) return jwk } } \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt index 46b83ff..4219fd3 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt @@ -155,7 +155,7 @@ object KeyPairUtil { ) else bytes } - fun generateEcPublicKeyJwk( + private fun publicKeyToJwk( ecPublicKey: ECPublicKey, option: SigningOption ): Map { @@ -172,7 +172,7 @@ object KeyPairUtil { ) } - fun generateRsaPublicKeyJwk(rsaPublicKey: RSAPublicKey): Map { + private fun publicKeyToJwk(rsaPublicKey: RSAPublicKey): Map { val n = Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()) val e = Base64.getUrlEncoder().encodeToString(rsaPublicKey.publicExponent.toByteArray()) @@ -184,18 +184,20 @@ object KeyPairUtil { ) } - fun generatePublicKeyJwk(keyPair: KeyPair, option: SigningOption): Map { + fun keyPairToPublicJwk(keyPair: KeyPair, option: SigningOption): Map { val publicKey: PublicKey = keyPair.public - return generatePublicKeyJwk(publicKey, option) + return publicKeyToJwk(publicKey, option) } - fun generatePublicKeyJwk(publicKey: PublicKey, option: SigningOption): Map { + + fun publicKeyToJwk(publicKey: PublicKey, option: SigningOption): Map { return when (publicKey) { - is RSAPublicKey -> generateRsaPublicKeyJwk(publicKey) - is ECPublicKey -> generateEcPublicKeyJwk(publicKey, option) + is RSAPublicKey -> publicKeyToJwk(publicKey) + is ECPublicKey -> publicKeyToJwk(publicKey, option) else -> throw IllegalArgumentException("Unsupported Key Type: ${publicKey::class.java.name}") } } + // todo move to anywhere else fun verifyJwt(jwkJson: Map, jwt: String): Boolean { val publicKey = createPublicKey(jwkJson) val result = JWT.verifyJwt(jwt, publicKey) From 66d8363cbab37ad20869d1fb145f7c9591eae9f8 Mon Sep 17 00:00:00 2001 From: ryousuke Date: Tue, 4 Jun 2024 15:36:14 +0900 Subject: [PATCH 5/7] add condition before deserialization --- .../main/java/com/ownd_project/tw2023_wallet_android/oid/URL.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/URL.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/URL.kt index 90c46dc..8020c96 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/URL.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/oid/URL.kt @@ -82,7 +82,7 @@ suspend fun parseAndResolve(uri: String): ParseAndResolveResult { propertyNamingStrategy = PropertyNamingStrategies.SNAKE_CASE configure(DeserializationFeature.READ_ENUMS_USING_TO_STRING, true) } - mapper.readValue(it.toString()) + if (!it.isMissing && !it.isNull) mapper.readValue(it.toString()) else null } println("client metadata: $clientMetadata") val clientMetadataUri = decodedJwt.getClaim("client_metadata_uri").asString()?: authorizationRequestPayload.clientMetadataUri From 12b6374c2ac75149c9e2c309afeb386a364a5f4b Mon Sep 17 00:00:00 2001 From: ryousuke Date: Tue, 4 Jun 2024 15:37:48 +0900 Subject: [PATCH 6/7] separate KeyUtil from KeyPairUtil not to load android store code when unit test --- .../oid/OpenIdProvider.kt | 4 +- .../ui/shared/JwtVpJsonGeneratorImpl.kt | 3 +- .../utils/KeyPairUtil.kt | 59 --------------- .../tw2023_wallet_android/utils/KeyUtil.kt | 73 +++++++++++++++++++ 4 files changed, 77 insertions(+), 62 deletions(-) create mode 100644 app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyUtil.kt 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 c24d3ed..affbfaa 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 @@ -15,7 +15,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.module.kotlin.convertValue import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.ownd_project.tw2023_wallet_android.utils.SigningOption -import com.ownd_project.tw2023_wallet_android.utils.KeyPairUtil +import com.ownd_project.tw2023_wallet_android.utils.KeyUtil import okhttp3.FormBody import okhttp3.OkHttpClient import okhttp3.Request @@ -205,7 +205,7 @@ class OpenIdProvider(val uri: String, val option: SigningOption = SigningOption( val nonce = authRequest.nonce val SEC_IN_MS = 1000 - val subJwk = KeyPairUtil.keyPairToPublicJwk(keyPair, option) + val subJwk = KeyUtil.keyPairToPublicJwk(keyPair, option) // todo: support rsa key val jwk = object : ECPublicJwk { override val kty = subJwk["kty"]!! diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt index 104c52c..0ead917 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/ui/shared/JwtVpJsonGeneratorImpl.kt @@ -8,6 +8,7 @@ import com.ownd_project.tw2023_wallet_android.oid.JwtVpJsonPayloadOptions import com.ownd_project.tw2023_wallet_android.signature.JWT import com.ownd_project.tw2023_wallet_android.utils.SigningOption import com.ownd_project.tw2023_wallet_android.utils.KeyPairUtil +import com.ownd_project.tw2023_wallet_android.utils.KeyUtil import java.security.PublicKey class JwtVpJsonGeneratorImpl(private val keyAlias: String = Constants.KEY_PAIR_ALIAS_FOR_KEY_JWT_VP_JSON) : @@ -48,7 +49,7 @@ class JwtVpJsonGeneratorImpl(private val keyAlias: String = Constants.KEY_PAIR_A } val publicKey: PublicKey = KeyPairUtil.getPublicKey(keyAlias) ?: throw IllegalStateException("Public key not found for alias: $keyAlias") - val jwk = KeyPairUtil.publicKeyToJwk(publicKey, SigningOption()) + val jwk = KeyUtil.publicKeyToJwk(publicKey, SigningOption()) return jwk } } \ No newline at end of file diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt index 4219fd3..13f615f 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt @@ -138,65 +138,6 @@ object KeyPairUtil { return keyFactory.generatePublic(keySpec) } - fun correctBytes(value: BigInteger): ByteArray { - /* - BigInteger の toByteArray() メソッドは、数値をバイト配列に変換しますが、 - この数値が正の場合、最上位バイトが符号ビットとして解釈されることを避けるために、追加のゼロバイトが先頭に挿入されることがあります。 - これは、数値が正で、最上位バイトが 0x80 以上の場合(つまり、最上位ビットが 1 の場合)に起こります。 - その結果、期待していた 32 バイトではなく 33 バイトの配列が得られることがあります。 - - 期待する 32 バイトの配列を得るには、返されたバイト配列から余分なゼロバイトを取り除くか、 - または正確なバイト長を指定して配列を生成する必要があります。 - */ - val bytes = value.toByteArray() - return if (bytes.size == 33 && bytes[0] == 0.toByte()) bytes.copyOfRange( - 1, - bytes.size - ) else bytes - } - - private fun publicKeyToJwk( - ecPublicKey: ECPublicKey, - option: SigningOption - ): Map { - val ecPoint: ECPoint = ecPublicKey.w - val x = correctBytes(ecPoint.affineX).toBase64Url() - val y = correctBytes(ecPoint.affineY).toBase64Url() - - // return """{"kty":"EC","crv":"P-256","x":"$x","y":"$y"}""" // crvは適宜変更してください - return mapOf( - "kty" to "EC", - "crv" to option.signingCurve, - "x" to x, - "y" to y - ) - } - - private fun publicKeyToJwk(rsaPublicKey: RSAPublicKey): Map { - val n = Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()) - val e = Base64.getUrlEncoder().encodeToString(rsaPublicKey.publicExponent.toByteArray()) - - // return """{"kty":"RSA","n":"$n","e":"$e"}""" - return mapOf( - "kty" to "RSA", - "n" to n, - "e" to e - ) - } - - fun keyPairToPublicJwk(keyPair: KeyPair, option: SigningOption): Map { - val publicKey: PublicKey = keyPair.public - return publicKeyToJwk(publicKey, option) - } - - fun publicKeyToJwk(publicKey: PublicKey, option: SigningOption): Map { - return when (publicKey) { - is RSAPublicKey -> publicKeyToJwk(publicKey) - is ECPublicKey -> publicKeyToJwk(publicKey, option) - else -> throw IllegalArgumentException("Unsupported Key Type: ${publicKey::class.java.name}") - } - } - // todo move to anywhere else fun verifyJwt(jwkJson: Map, jwt: String): Boolean { val publicKey = createPublicKey(jwkJson) diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyUtil.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyUtil.kt new file mode 100644 index 0000000..3ddf610 --- /dev/null +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyUtil.kt @@ -0,0 +1,73 @@ +package com.ownd_project.tw2023_wallet_android.utils + +import com.ownd_project.tw2023_wallet_android.signature.toBase64Url +import java.math.BigInteger +import java.security.KeyPair +import java.security.PublicKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.ECPoint +import java.util.Base64 + +object KeyUtil { + + fun correctBytes(value: BigInteger): ByteArray { + /* + BigInteger の toByteArray() メソッドは、数値をバイト配列に変換しますが、 + この数値が正の場合、最上位バイトが符号ビットとして解釈されることを避けるために、追加のゼロバイトが先頭に挿入されることがあります。 + これは、数値が正で、最上位バイトが 0x80 以上の場合(つまり、最上位ビットが 1 の場合)に起こります。 + その結果、期待していた 32 バイトではなく 33 バイトの配列が得られることがあります。 + + 期待する 32 バイトの配列を得るには、返されたバイト配列から余分なゼロバイトを取り除くか、 + または正確なバイト長を指定して配列を生成する必要があります。 + */ + val bytes = value.toByteArray() + return if (bytes.size == 33 && bytes[0] == 0.toByte()) bytes.copyOfRange( + 1, + bytes.size + ) else bytes + } + + private fun publicKeyToJwk( + ecPublicKey: ECPublicKey, + option: SigningOption + ): Map { + val ecPoint: ECPoint = ecPublicKey.w + val x = correctBytes(ecPoint.affineX).toBase64Url() + val y = correctBytes(ecPoint.affineY).toBase64Url() + + // return """{"kty":"EC","crv":"P-256","x":"$x","y":"$y"}""" // crvは適宜変更してください + return mapOf( + "kty" to "EC", + "crv" to option.signingCurve, + "x" to x, + "y" to y + ) + } + + private fun publicKeyToJwk(rsaPublicKey: RSAPublicKey): Map { + val n = Base64.getUrlEncoder().encodeToString(rsaPublicKey.modulus.toByteArray()) + val e = Base64.getUrlEncoder().encodeToString(rsaPublicKey.publicExponent.toByteArray()) + + // return """{"kty":"RSA","n":"$n","e":"$e"}""" + return mapOf( + "kty" to "RSA", + "n" to n, + "e" to e + ) + } + + fun keyPairToPublicJwk(keyPair: KeyPair, option: SigningOption): Map { + val publicKey: PublicKey = keyPair.public + return publicKeyToJwk(publicKey, option) + } + + fun publicKeyToJwk(publicKey: PublicKey, option: SigningOption): Map { + return when (publicKey) { + is RSAPublicKey -> publicKeyToJwk(publicKey) + is ECPublicKey -> publicKeyToJwk(publicKey, option) + else -> throw IllegalArgumentException("Unsupported Key Type: ${publicKey::class.java.name}") + } + } + +} From e0484b6ee9761e1ae1d3e46e29c81379b498a5dd Mon Sep 17 00:00:00 2001 From: ryousuke Date: Tue, 4 Jun 2024 15:56:49 +0900 Subject: [PATCH 7/7] Refactor code for readability --- .../tw2023_wallet_android/utils/KeyPairUtil.kt | 8 +------- .../ownd_project/tw2023_wallet_android/utils/KeyUtil.kt | 5 +++++ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt index 13f615f..bc1514f 100644 --- a/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt +++ b/app/src/main/java/com/ownd_project/tw2023_wallet_android/utils/KeyPairUtil.kt @@ -2,8 +2,8 @@ package com.ownd_project.tw2023_wallet_android.utils import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties -import com.ownd_project.tw2023_wallet_android.signature.JWT import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.ownd_project.tw2023_wallet_android.signature.JWT import com.ownd_project.tw2023_wallet_android.signature.toBase64Url import org.jose4j.jwk.EllipticCurveJsonWebKey import org.jose4j.jwk.JsonWebKey @@ -17,9 +17,7 @@ import java.security.PrivateKey import java.security.PublicKey import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey -import java.security.interfaces.RSAPublicKey import java.security.spec.ECGenParameterSpec -import java.security.spec.ECPoint import java.security.spec.X509EncodedKeySpec import java.util.Base64 import javax.crypto.KeyGenerator @@ -218,7 +216,3 @@ fun privateKeyToJwk(privateKey: ECPrivateKey, publicKey: ECPublicKey): Map