Skip to content
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
@@ -1,11 +1,13 @@
package io.mosip.openID4VP.authorizationResponse

import com.fasterxml.jackson.annotation.JsonInclude
import com.fasterxml.jackson.core.type.TypeReference
import com.google.gson.annotations.SerializedName
import io.mosip.openID4VP.authorizationResponse.presentationSubmission.PresentationSubmission
import io.mosip.openID4VP.authorizationResponse.vpToken.VPTokenType
import io.mosip.openID4VP.authorizationResponse.vpToken.VPToken
import io.mosip.openID4VP.authorizationResponse.vpToken.VPTokenType
import io.mosip.openID4VP.common.encodeToJsonString
import io.mosip.openID4VP.common.encodeToJsonString
import io.mosip.openID4VP.common.getObjectMapper

private val className: String = AuthorizationResponse::class.simpleName!!

Expand Down Expand Up @@ -36,11 +38,29 @@ fun AuthorizationResponse.toJsonEncodedMap(): Map<String, String> {
className
)
)
state?.let<String, Unit> { put("state", it) }
state?.let { put("state", it) }
}
is AuthorizationResponse.Dcql -> buildMap {
put("vp_token", encodeToJsonString(vpToken, "vp_token", className))
state?.let<String, Unit> { put("state", it) }
state?.let { put("state", it) }
}
}
}

fun AuthorizationResponse.toMap(): Map<String, Any> {
val objectMapper = getObjectMapper().copy().setSerializationInclusion(JsonInclude.Include.NON_NULL)
return when (this) {
is AuthorizationResponse.PresentationExchange -> buildMap {
put("vp_token", objectMapper.convertValue(vpToken, object : TypeReference<Any>() {}))
put(
"presentation_submission",
objectMapper.convertValue(presentationSubmission, object : TypeReference<Map<String, Any>>() {})
)
state?.let { put("state", it) }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
is AuthorizationResponse.Dcql -> buildMap {
put("vp_token", objectMapper.convertValue(vpToken, object : TypeReference<Any>() {}))
state?.let { put("state", it) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,17 @@ sealed class OpenID4VPExceptions(
className
)

class JweEncryptionFailure(className: String, cause: Throwable? = null) :
class UnsupportedOperationException(message: String, className: String) :
OpenID4VPExceptions(
INVALID_REQUEST,
"JWE Encryption failed",
message,
className
)

class JweEncryptionFailure(message: String, className: String, cause: Throwable? = null) :
OpenID4VPExceptions(
INVALID_REQUEST,
message,
className,
cause = cause
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.nimbusds.jose.JWEObject
import com.nimbusds.jose.Payload
import com.nimbusds.jose.util.Base64URL
import io.mosip.openID4VP.authorizationRequest.clientMetadata.Jwk
import io.mosip.openID4VP.common.getObjectMapper
import io.mosip.openID4VP.exceptions.OpenID4VPExceptions
import io.mosip.openID4VP.jwt.jwe.encryption.EncryptionProvider

Expand All @@ -21,9 +22,13 @@ class JWEHandler(
) {

fun generateEncryptedResponse(payload: Map<String, Any>): String {
if (keyEncryptionAlg != "ECDH-ES" || contentEncryptionAlg != "A256GCM") {
throw OpenID4VPExceptions.UnsupportedOperationException(
"Unsupported encryption configuration: keyEncryptionAlgorithm=$keyEncryptionAlg, contentEncryptionAlgorithm=$contentEncryptionAlg. Supported configuration: keyEncryptionAlgorithm=ECDH-ES, contentEncryptionAlgorithm=A256GCM",
className
)
}
try {
val payloadString = io.mosip.openID4VP.common.getObjectMapper().writeValueAsString(payload)

val header = JWEHeader.Builder(
JWEAlgorithm.parse(keyEncryptionAlg),
EncryptionMethod.parse(contentEncryptionAlg)
Expand All @@ -34,12 +39,16 @@ class JWEHandler(
.build()

val encrypter = EncryptionProvider.getEncrypter(publicKey)
val jweObject = JWEObject(header, Payload(payloadString))
val jweObject = JWEObject(header, Payload(payload))
jweObject.encrypt(encrypter)

return jweObject.serialize()
} catch (exception: Exception) {
throw OpenID4VPExceptions.JweEncryptionFailure(className, exception)
throw OpenID4VPExceptions.JweEncryptionFailure(
"JWE Encryption failed : " + (exception.message ?: "Unknown error"),
className,
exception
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,66 @@
package io.mosip.openID4VP.jwt.jwe.encryption

import com.nimbusds.jose.Algorithm
import com.nimbusds.jose.EncryptionMethod
import com.nimbusds.jose.JWEEncrypter
import com.nimbusds.jose.crypto.ECDHEncrypter
import com.nimbusds.jose.crypto.X25519Encrypter
import com.nimbusds.jose.jwk.Curve
import com.nimbusds.jose.jwk.ECKey
import com.nimbusds.jose.jwk.KeyType
import com.nimbusds.jose.jwk.KeyUse
import com.nimbusds.jose.jwk.OctetKeyPair
import com.nimbusds.jose.util.Base64URL
import io.mosip.openID4VP.authorizationRequest.clientMetadata.Jwk
import io.mosip.openID4VP.constants.EncryptionAlgorithm
import io.mosip.openID4VP.exceptions.OpenID4VPExceptions

private val className = EncryptionProvider::class.simpleName!!
object EncryptionProvider {

fun getEncrypter(jwk: Jwk): JWEEncrypter =
when (jwk.kty) {
KeyType.OKP.value -> X25519Encrypter(getPublicOctetKey(jwk))
private val supportedEncryptionAlgs = setOf(
EncryptionAlgorithm.ECDH_ES.value
)

fun getEncrypter(jwk: Jwk): JWEEncrypter {
if (jwk.alg !in supportedEncryptionAlgs) {
throw OpenID4VPExceptions.JweEncryptionFailure(
"Unsupported JWE algorithm: ${jwk.alg}",
className
)
}

return when (jwk.kty) {
KeyType.OKP.value -> {
if (jwk.crv != Curve.X25519.name) {
throw OpenID4VPExceptions.JweEncryptionFailure(
"Unsupported OKP curve for ECDH-ES: ${jwk.crv}. Only X25519 is supported.",
className
)
}

X25519Encrypter(getPublicOctetKey(jwk))
}

KeyType.EC.value -> {
if (
jwk.crv !in setOf(
Curve.P_256.name,
Curve.P_384.name,
Curve.P_521.name
)
) {
throw OpenID4VPExceptions.JweEncryptionFailure(
"Unsupported EC curve for ECDH-ES: ${jwk.crv}. Only P-256, P-384, P-521 are supported.",
className
)
}

ECDHEncrypter(getPublicEcKey(jwk))
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

else -> throw OpenID4VPExceptions.UnsupportedKeyExchangeAlgorithm(className)
}
}

private fun getPublicOctetKey(jwk: Jwk): OctetKeyPair {
val builder = OctetKeyPair.Builder(Curve(jwk.crv), Base64URL.from(jwk.x))
Expand All @@ -32,4 +73,19 @@ object EncryptionProvider {
.build()
.toPublicJWK()
}

private fun getPublicEcKey(jwk: Jwk): ECKey {
val y = jwk.y ?: throw OpenID4VPExceptions.UnsupportedKeyExchangeAlgorithm(className)
val builder = ECKey.Builder(
Curve(jwk.crv),
Base64URL.from(jwk.x),
Base64URL.from(y)
)
.keyID(jwk.kid)
.algorithm(Algorithm.parse(jwk.alg))

jwk.use?.let { builder.keyUse(KeyUse(it)) }

return builder.build().toPublicJWK()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.mosip.openID4VP.authorizationRequest.clientMetadata.Jwk
import io.mosip.openID4VP.authorizationResponse.AuthorizationErrorResponse
import io.mosip.openID4VP.authorizationResponse.AuthorizationResponse
import io.mosip.openID4VP.authorizationResponse.toJsonEncodedMap
import io.mosip.openID4VP.authorizationResponse.toMap
import io.mosip.openID4VP.constants.EncryptionMethod
import io.mosip.openID4VP.jwt.jwe.JWEHandler
import io.mosip.openID4VP.constants.ContentType
Expand Down Expand Up @@ -149,7 +150,7 @@ class DirectPostJwtResponseModeHandler : ResponseModeBasedHandler() {
): Map<String, String> {
return encryptResponse(
authorizationRequest, walletNonce,
authorizationResponse.toJsonEncodedMap(),
authorizationResponse.toMap(),
walletConfig
)
}
Expand All @@ -173,7 +174,7 @@ class DirectPostJwtResponseModeHandler : ResponseModeBasedHandler() {
private fun encryptResponse(
authorizationRequest: AuthorizationRequest,
walletNonce: String,
responseParams: Map<String, String>,
responseParams: Map<String, Any>,
walletConfig: WalletConfig
): Map<String, String> {
val specVersionHandler = SpecVersionHandler.from(authorizationRequest)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,96 @@ class AuthorizationResponseTest {
assertTrue(map.containsKey("presentation_submission"))
assertFalse(map.containsKey("state"))
}

@Test
fun `toMap should filter out null values`() {
val responseWithNullState = AuthorizationResponse.PresentationExchange(
presentationSubmission = presentationSubmission,
vpToken = vpToken,
state = null
)
val map = responseWithNullState.toMap()
assertEquals(2, map.size)
assertTrue(map.containsKey("vp_token"))
assertTrue(map.containsKey("presentation_submission"))
assertFalse(map.containsKey("state"))
}

@Test
fun `toMap should return exact expected map`() {
val expectedVpTokenMap = mapOf(
"@context" to listOf("context"),
"type" to listOf("type"),
"verifiableCredential" to listOf("VC1"),
"id" to "id",
"holder" to "holder",
"proof" to mapOf(
"type" to "type",
"created" to "time",
"challenge" to "challenge",
"domain" to "domain",
"proofValue" to "eryy....ewr",
"proofPurpose" to "authentication",
"verificationMethod" to "did:example:holder#key-1"
)
)

val expectedPresentationSubmissionMap = mapOf(
"id" to "ps_id",
"definition_id" to "client_id",
"descriptor_map" to listOf(
mapOf(
"id" to "input_descriptor_1",
"format" to "ldp_vp",
"path" to "$",
"path_nested" to mapOf(
"id" to "input_descriptor_1",
"format" to "ldp_vp",
"path" to "$.verifiableCredential[0]"
)
)
)
)

val expectedMap = mapOf(
"vp_token" to expectedVpTokenMap,
"presentation_submission" to expectedPresentationSubmissionMap,
"state" to "state"
)

val actualMap = authorizationResponse.toMap()
assertEquals(expectedMap, actualMap)
}

@Test
fun `toMap should return objects for Dcql response`() {
val dcqlResponse = AuthorizationResponse.Dcql(
vpToken = mapOf("credential1" to listOf(ldpVPToken)),
state = "state_123"
)
val expectedVpTokenMap = mapOf(
"@context" to listOf("context"),
"type" to listOf("type"),
"verifiableCredential" to listOf("VC1"),
"id" to "id",
"holder" to "holder",
"proof" to mapOf(
"type" to "type",
"created" to "time",
"challenge" to "challenge",
"domain" to "domain",
"proofValue" to "eryy....ewr",
"proofPurpose" to "authentication",
"verificationMethod" to "did:example:holder#key-1"
)
)

val expectedMap = mapOf(
"vp_token" to mapOf("credential1" to listOf(expectedVpTokenMap)),
"state" to "state_123"
)

val actualMap = dcqlResponse.toMap()
assertEquals(expectedMap, actualMap)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class JWEHandlerTest {
@BeforeTest
fun setUp() {
clientMetadata = deserializeAndValidate(clientMetadataString, ClientMetadataDraft23Serializer)
publicKey = clientMetadata.jwks!!.keys[0]
publicKey = clientMetadata.jwks!!.keys[1]
jweHandler = JWEHandler(
"ECDH-ES",
"A256GCM",
Expand Down Expand Up @@ -74,6 +74,32 @@ class JWEHandlerTest {
val exception = assertFailsWith<OpenID4VPExceptions.JweEncryptionFailure> {
handler.generateEncryptedResponse(payload)
}
assertEquals(true, exception.message?.startsWith("JWE Encryption failed"))
assertTrue(exception.message!!.contains("JWE Encryption failed"))
}

@Test
fun `should throw UnsupportedOperationException when keyEncryptionAlg is unsupported`() {
val payload = mapOf("key1" to "value1")
val handler = JWEHandler("RSA-OAEP", "A256GCM", publicKey, walletNonce, verifierNonce)

val exception = assertFailsWith<OpenID4VPExceptions.UnsupportedOperationException> {
handler.generateEncryptedResponse(payload)
}

val expectedMessage = "Unsupported encryption configuration: keyEncryptionAlgorithm=RSA-OAEP, contentEncryptionAlgorithm=A256GCM. Supported configuration: keyEncryptionAlgorithm=ECDH-ES, contentEncryptionAlgorithm=A256GCM"
assertTrue(exception.message.contains(expectedMessage))
}

@Test
fun `should throw UnsupportedOperationException when contentEncryptionAlg is unsupported`() {
val payload = mapOf("key1" to "value1")
val handler = JWEHandler("ECDH-ES", "A128GCM", publicKey, walletNonce, verifierNonce)

val exception = assertFailsWith<OpenID4VPExceptions.UnsupportedOperationException> {
handler.generateEncryptedResponse(payload)
}

val expectedMessage = "Unsupported encryption configuration: keyEncryptionAlgorithm=ECDH-ES, contentEncryptionAlgorithm=A128GCM. Supported configuration: keyEncryptionAlgorithm=ECDH-ES, contentEncryptionAlgorithm=A256GCM"
assertTrue(exception.message.contains(expectedMessage))
}
}
Loading
Loading