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
4 changes: 2 additions & 2 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ espresso-core = "3.6.1"
eudi-document-manager = "0.13.0"
eudi-iso18013-data-transfer = "0.10.0"
eudi-lib-jvm-openid4vci-kt = "0.9.1"
eudi-lib-jvm-siop-openid4vp-kt = "0.11.1"
eudi-lib-jvm-siop-openid4vp-kt = "0.12.0"
eudi-lib-jvm-sdjwt-kt = "0.10.0"
eudi-lib-kmp-statium = "0.4.0"
gradle-plugin = "8.13.0"
Expand Down Expand Up @@ -60,7 +60,7 @@ espresso-intents = { module = "androidx.test.espresso:espresso-intents", version
eudi-document-manager = { module = "eu.europa.ec.eudi:eudi-lib-android-wallet-document-manager", version.ref = "eudi-document-manager" }
eudi-iso18013-data-transfer = { module = "eu.europa.ec.eudi:eudi-lib-android-iso18013-data-transfer", version.ref = "eudi-iso18013-data-transfer" }
eudi-lib-jvm-openid4vci-kt = { module = "eu.europa.ec.eudi:eudi-lib-jvm-openid4vci-kt", version.ref = "eudi-lib-jvm-openid4vci-kt" }
eudi-lib-jvm-siop-openid4vp-kt = { module = "eu.europa.ec.eudi:eudi-lib-jvm-siop-openid4vp-kt", version.ref = "eudi-lib-jvm-siop-openid4vp-kt" }
eudi-lib-jvm-siop-openid4vp-kt = { module = "eu.europa.ec.eudi:eudi-lib-jvm-openid4vp-kt", version.ref = "eudi-lib-jvm-siop-openid4vp-kt" }
eudi-lib-jvm-sdjwt-kt = { module = "eu.europa.ec.eudi:eudi-lib-jvm-sdjwt-kt", version.ref = "eudi-lib-jvm-sdjwt-kt" }
eudi-lib-kmp-statium = { module = "eu.europa.ec.eudi:eudi-lib-kmp-statium-android", version.ref = "eudi-lib-kmp-statium" }
json = { module = "org.json:json", version.ref = "json" }
Expand Down
2 changes: 1 addition & 1 deletion wallet-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ dependencies {
// OpenID4VCI
implementation(libs.nimbus.oauth2.oidc.sdk)
// Siop-Openid4VP library
implementation(libs.eudi.lib.jvm.siop.openid4vp.kt) {
api(libs.eudi.lib.jvm.siop.openid4vp.kt) {
exclude(group = "org.bouncycastle")
}
// SD-JWT VC library
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import java.util.Locale
import kotlin.io.encoding.ExperimentalEncodingApi
import androidx.core.graphics.scale

private const val SHA_256_ALGORITHM = "SHA-256"

/**
* Utility functions for DCAPI.
*/
Expand Down Expand Up @@ -89,7 +91,7 @@ internal fun getDCAPIIsoMdocSessionTranscript(encryptionInfoBase64: String, orig
Add(encryptionInfoBase64)
Add(origin)
}.EncodeToBytes()
val dcapiInfoHash = MessageDigest.getInstance("SHA-256").digest(dcapiInfo)
val dcapiInfoHash = MessageDigest.getInstance(SHA_256_ALGORITHM).digest(dcapiInfo)
val dcapiIsoMdocHandover = CBORObject.NewArray().apply {
Add(DCAPI)
Add(dcapiInfoHash)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,13 @@ import eu.europa.ec.eudi.iso18013.transfer.response.device.ProcessedDeviceReques
import eu.europa.ec.eudi.openid4vp.CoseAlgorithm
import eu.europa.ec.eudi.openid4vp.JarConfiguration
import eu.europa.ec.eudi.openid4vp.JwkSetSource.ByReference
import eu.europa.ec.eudi.openid4vp.OpenId4VPConfig
import eu.europa.ec.eudi.openid4vp.PreregisteredClient
import eu.europa.ec.eudi.openid4vp.ResolvedRequestObject
import eu.europa.ec.eudi.openid4vp.ResponseEncryptionConfiguration
import eu.europa.ec.eudi.openid4vp.ResponseMode
import eu.europa.ec.eudi.openid4vp.SiopOpenId4VPConfig
import eu.europa.ec.eudi.openid4vp.SupportedClientIdPrefix
import eu.europa.ec.eudi.openid4vp.TransactionData
import eu.europa.ec.eudi.openid4vp.VPConfiguration
import eu.europa.ec.eudi.openid4vp.VerifiablePresentation
import eu.europa.ec.eudi.openid4vp.VerifierId
Expand Down Expand Up @@ -84,6 +85,8 @@ import java.security.SecureRandom
import java.util.Base64
import java.util.Date

private const val SHA_256_ALGORITHM = "SHA-256"

/**
* Utility to generate the session transcript for the OpenID4VP protocol.
*
Expand Down Expand Up @@ -164,8 +167,8 @@ internal fun generateOpenId4VpHandover(
Add(mdocGeneratedNonce)
}.EncodeToBytes()

val clientIdHash = MessageDigest.getInstance("SHA-256").digest(clientIdToHash)
val responseUriHash = MessageDigest.getInstance("SHA-256").digest(responseUriToHash)
val clientIdHash = MessageDigest.getInstance(SHA_256_ALGORITHM).digest(clientIdToHash)
val responseUriHash = MessageDigest.getInstance(SHA_256_ALGORITHM).digest(responseUriToHash)

val openID4VPHandover = CBORObject.NewArray().apply {
Add(clientIdHash)
Expand All @@ -187,10 +190,10 @@ internal fun generateMdocGeneratedNonce(): String {
return Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)
}

internal fun SiopOpenId4VPConfig.Companion.make(
internal fun makeOpenId4VPConfig(
config: OpenId4VpConfig,
trust: OpenId4VpReaderTrust,
): SiopOpenId4VPConfig {
): OpenId4VPConfig {
val supportedClientIdPrefixes = config.clientIdSchemes.map { clientIdScheme ->
when (clientIdScheme) {
is ClientIdScheme.Preregistered -> SupportedClientIdPrefix.Preregistered(
Expand All @@ -211,8 +214,8 @@ internal fun SiopOpenId4VPConfig.Companion.make(
ClientIdScheme.X509Hash -> SupportedClientIdPrefix.X509Hash(trust = trust)
}
}
return SiopOpenId4VPConfig(
issuer = SelfIssued,
return OpenId4VPConfig(
issuer = OpenId4VPConfig.SelfIssued,
jarConfiguration = JarConfiguration.Default,
responseEncryptionConfiguration = ResponseEncryptionConfiguration.Supported(
supportedAlgorithms = config.encryptionAlgorithms.map { it.nimbus },
Expand All @@ -225,23 +228,13 @@ internal fun SiopOpenId4VPConfig.Companion.make(
)
}

/**
* Converts an [OpenId4VpConfig] to a [SiopOpenId4VPConfig] using the provided trust anchor.
*
* @param trust The trust anchor for reader verification.
* @return The corresponding [SiopOpenId4VPConfig].
*/
internal fun OpenId4VpConfig.toSiopOpenId4VPConfig(trust: OpenId4VpReaderTrust): SiopOpenId4VPConfig {
return SiopOpenId4VPConfig.make(this, trust)
}

/**
* Extension function to get the session transcript bytes from a resolved OpenID4VP authorization request.
*
* @param mdocGeneratedNonce The generated nonce for mdoc.
* @return The session transcript as a byte array.
*/
internal fun ResolvedRequestObject.OpenId4VPAuthorization.getSessionTranscriptBytes(
internal fun ResolvedRequestObject.getSessionTranscriptBytes(
mdocGeneratedNonce: String,
): SessionTranscriptBytes {
val clientId = this.client.id.clientId
Expand Down Expand Up @@ -327,6 +320,7 @@ internal val EncryptionMethod.nimbus: com.nimbusds.jose.EncryptionMethod
* @param nonce The nonce for the session.
* @param signatureAlgorithm The algorithm to use for signing.
* @param issueDate The date of issuance.
* @param transactionData Optional list of transaction data
* @return The serialized SD-JWT as a string.
*/
internal suspend fun SdJwt<JwtAndClaims>.serializeWithKeyBinding(
Expand All @@ -336,6 +330,7 @@ internal suspend fun SdJwt<JwtAndClaims>.serializeWithKeyBinding(
nonce: String,
signatureAlgorithm: Algorithm,
issueDate: Date,
transactionData: List<TransactionData.SdJwtVc>? = null,
): String {
val algorithm = JWSAlgorithm.parse((signatureAlgorithm).joseAlgorithmIdentifier)
val publicKey = credential.secureArea.getKeyInfo(credential.alias).publicKey
Expand All @@ -360,25 +355,40 @@ internal suspend fun SdJwt<JwtAndClaims>.serializeWithKeyBinding(
audience(clientId.clientId)
claim("nonce", nonce)
issueTime(issueDate)
if (!transactionData.isNullOrEmpty()) {
val transactionDataHashes = transactionData.map { td ->
computeTransactionDataHash(td.value)
}
claim("transaction_data_hashes", transactionDataHashes)
claim("transaction_data_hashes_alg", "sha-256")
}
}
return serializeWithKeyBinding(buildKbJwt).getOrThrow()
}

internal fun computeTransactionDataHash(transactionDataValue: String): String {
val digest = MessageDigest.getInstance(SHA_256_ALGORITHM)
digest.update(transactionDataValue.encodeToByteArray())
return Base64.getUrlEncoder().withoutPadding().encodeToString(digest.digest())
}

/**
* Constructs a verifiable presentation for an SD-JWT VC credential.
*
* @param resolvedRequestObject The resolved OpenID4VP authorization request.
* @param document The issued document containing the credential.
* @param disclosedDocument The document with disclosed claims.
* @param signatureAlgorithm The algorithm to use for signing.
* @param transactionData Optional list of SD-JWT VC transaction data applicable to this presentation.
* @return The constructed [VerifiablePresentation.Generic].
* @throws IllegalArgumentException if no claims are disclosed or presentation creation fails.
*/
internal suspend fun verifiablePresentationForSdJwtVc(
resolvedRequestObject: ResolvedRequestObject.OpenId4VPAuthorization,
resolvedRequestObject: ResolvedRequestObject,
document: IssuedDocument,
disclosedDocument: DisclosedDocument,
signatureAlgorithm: Algorithm,
transactionData: List<TransactionData.SdJwtVc>? = null,
): VerifiablePresentation.Generic {
return document.consumingCredential {
val credentialIssuedData =
Expand Down Expand Up @@ -409,7 +419,8 @@ internal suspend fun verifiablePresentationForSdJwtVc(
clientId = resolvedRequestObject.client.id,
nonce = resolvedRequestObject.nonce,
signatureAlgorithm = signatureAlgorithm,
issueDate = Date()
issueDate = Date(),
transactionData = transactionData
)
} else {
presentation.serialize()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
* JSON serialization/deserialization capabilities for:
* - VP Token consensus data
* - Verifiable presentations collections
*
* - Individual verifiable presentation objects
*/
package eu.europa.ec.eudi.wallet.transactionLogging.presentation
Expand Down Expand Up @@ -52,7 +53,7 @@ import kotlinx.serialization.modules.SerializersModule


val module = SerializersModule {
contextual(Consensus.PositiveConsensus.VPTokenConsensus::class, VPTokenConsensusSerializer)
contextual(Consensus.PositiveConsensus::class, VPTokenConsensusSerializer)
}

val VPTokenConsensusJson = Json {
Expand All @@ -61,16 +62,16 @@ val VPTokenConsensusJson = Json {
}

/**
* Custom serializer for [Consensus.PositiveConsensus.VPTokenConsensus] objects.
* Custom serializer for [Consensus.PositiveConsensus] objects.
*
* This serializer handles the serialization and deserialization of VP Token consensus data,
* which contains verifiable presentations that have been agreed upon during the consensus process.
* The serializer delegates the actual presentations serialization to [VerifiablePresentationsSerializer].
*
* @see Consensus.PositiveConsensus.VPTokenConsensus
* @see Consensus.PositiveConsensus
* @see VerifiablePresentationsSerializer
*/
object VPTokenConsensusSerializer : KSerializer<Consensus.PositiveConsensus.VPTokenConsensus> {
object VPTokenConsensusSerializer : KSerializer<Consensus.PositiveConsensus> {

/**
* Serial descriptor for the VPTokenConsensus structure.
Expand All @@ -81,12 +82,12 @@ object VPTokenConsensusSerializer : KSerializer<Consensus.PositiveConsensus.VPTo
}

/**
* Serializes a [Consensus.PositiveConsensus.VPTokenConsensus] object to the encoder.
* Serializes a [Consensus.PositiveConsensus] object to the encoder.
*
* @param encoder The encoder to write the serialized data to
* @param value The VPTokenConsensus object to serialize
* @param value The PositiveConsensus object to serialize
*/
override fun serialize(encoder: Encoder, value: Consensus.PositiveConsensus.VPTokenConsensus) {
override fun serialize(encoder: Encoder, value: Consensus.PositiveConsensus) {
encoder.encodeStructure(descriptor) {
encodeSerializableElement(
descriptor,
Expand All @@ -98,13 +99,13 @@ object VPTokenConsensusSerializer : KSerializer<Consensus.PositiveConsensus.VPTo
}

/**
* Deserializes a [Consensus.PositiveConsensus.VPTokenConsensus] object from the decoder.
* Deserializes a [Consensus.PositiveConsensus] object from the decoder.
*
* @param decoder The decoder to read the serialized data from
* @return The deserialized VPTokenConsensus object
* @return The deserialized PositiveConsensus object
* @throws SerializationException if the required verifiablePresentations field is missing
*/
override fun deserialize(decoder: Decoder): Consensus.PositiveConsensus.VPTokenConsensus {
override fun deserialize(decoder: Decoder): Consensus.PositiveConsensus {
return decoder.decodeStructure(descriptor) {
var verifiablePresentations: VerifiablePresentations? = null
while (true) {
Expand All @@ -118,7 +119,7 @@ object VPTokenConsensusSerializer : KSerializer<Consensus.PositiveConsensus.VPTo
else -> error("Unexpected index: $index")
}
}
Consensus.PositiveConsensus.VPTokenConsensus(
Consensus.PositiveConsensus(
verifiablePresentations = verifiablePresentations
?: throw SerializationException("Missing verifiablePresentations")
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class TransactionLogBuilder(
* It handles different types of requests:
* - [DeviceRequest]: Stores the raw request bytes.
* - [OpenId4VpRequest]: Extracts and stores the presentation definition or digital credentials query from the resolved request object.
* Requires the resolved request to be [ResolvedRequestObject.OpenId4VPAuthorization] and
* Requires the resolved request to be [ResolvedRequestObject] and
* - Other request types: Marks the log status as [TransactionLog.Status.Error].
*
* The timestamp of the log is updated to the current time.
Expand All @@ -93,9 +93,6 @@ class TransactionLogBuilder(

is OpenId4VpRequest -> {
val resolvedRequestObject = request.resolvedRequestObject
require(resolvedRequestObject is ResolvedRequestObject.OpenId4VPAuthorization) {
"Only OpenId4VPAuthorization is supported"
}
val rawRequest = Json.encodeToString(resolvedRequestObject.query).toByteArray()

log.copy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ fun parseVp(
): List<PresentedDocument> {
val parsedMetadata = metadata.map { TransactionLog.Metadata.fromJson(it) }
val vpToken =
VPTokenConsensusJson.decodeFromString<Consensus.PositiveConsensus.VPTokenConsensus>(
VPTokenConsensusJson.decodeFromString<Consensus.PositiveConsensus>(
String(rawResponse)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,13 @@ import eu.europa.ec.eudi.iso18013.transfer.readerauth.ReaderTrustStore
import eu.europa.ec.eudi.iso18013.transfer.readerauth.ReaderTrustStoreAware
import eu.europa.ec.eudi.iso18013.transfer.response.Response
import eu.europa.ec.eudi.openid4vp.DispatchOutcome
import eu.europa.ec.eudi.openid4vp.OpenId4Vp
import eu.europa.ec.eudi.openid4vp.Resolution
import eu.europa.ec.eudi.openid4vp.ResolvedRequestObject
import eu.europa.ec.eudi.openid4vp.SiopOpenId4Vp
import eu.europa.ec.eudi.openid4vp.asException
import eu.europa.ec.eudi.wallet.internal.d
import eu.europa.ec.eudi.wallet.internal.e
import eu.europa.ec.eudi.wallet.internal.i
import eu.europa.ec.eudi.wallet.internal.toSiopOpenId4VPConfig
import eu.europa.ec.eudi.wallet.internal.makeOpenId4VPConfig
import eu.europa.ec.eudi.wallet.internal.wrappedWithContentNegotiation
import eu.europa.ec.eudi.wallet.internal.wrappedWithLogging
import eu.europa.ec.eudi.wallet.logging.Logger
Expand Down Expand Up @@ -79,13 +78,14 @@ class OpenId4VpManager(
}

/**
* Lazy initialization of the SIOP OpenID4VP protocol handler with logging and content negotiation.
* Lazy initialization of the OpenID4VP protocol handler with logging and content negotiation.
* Uses the configuration and trust anchor from the request processor.
*/
private val siopOpenId4Vp by lazy {
SiopOpenId4Vp(
siopOpenId4VPConfig = config.toSiopOpenId4VPConfig(
trust = requestProcessor.openid4VpX509CertificateTrust
private val openId4Vp by lazy {
OpenId4Vp(
openId4VPConfig = makeOpenId4VPConfig(
config,
requestProcessor.openid4VpX509CertificateTrust
),
httpClient = (ktorHttpClientFactory ?: DefaultHttpClientFactory)
.wrappedWithLogging(logger)
Expand Down Expand Up @@ -149,7 +149,7 @@ class OpenId4VpManager(
require(config.schemes.contains(Uri.parse(uri).scheme)) {
"Not supported scheme for OpenId4Vp"
}
when (val resolution = siopOpenId4Vp.resolveRequestUri(uri)) {
when (val resolution = openId4Vp.resolveRequestUri(uri)) {
is Resolution.Invalid -> {

// TODO dispatch error to verifier/RP
Expand All @@ -163,25 +163,13 @@ class OpenId4VpManager(

is Resolution.Success -> {
logger?.d(TAG, "Resolution.Success")
when (val resolvedRequest = resolution.requestObject) {
is ResolvedRequestObject.OpenId4VPAuthorization -> {
logger?.i(TAG, "${resolvedRequest::class.simpleName} received")
val request = OpenId4VpRequest(resolvedRequest)
val processedRequest = requestProcessor.process(request)
transferEventListeners.onTransferEvent(
TransferEvent.RequestReceived(processedRequest, request)
)
}

is ResolvedRequestObject.SiopAuthentication,
is ResolvedRequestObject.SiopOpenId4VPAuthentication,
-> {
logger?.i(TAG, "${resolvedRequest::class.simpleName} received")
transferEventListeners.onTransferEvent(
TransferEvent.Error(IllegalArgumentException("Unsupported request type"))
)
}
}
val resolvedRequest = resolution.requestObject
logger?.i(TAG, "${resolvedRequest::class.simpleName} received")
val request = OpenId4VpRequest(resolvedRequest)
val processedRequest = requestProcessor.process(request)
transferEventListeners.onTransferEvent(
TransferEvent.RequestReceived(processedRequest, request)
)
}
}
} catch (e: Throwable) {
Expand Down Expand Up @@ -212,13 +200,10 @@ class OpenId4VpManager(
require(response is OpenId4VpResponse) {
"Response must be an OpenId4VpResponse"
}
require(response.resolvedRequestObject is ResolvedRequestObject.OpenId4VPAuthorization) {
"Resolved request object must be OpenId4VPAuthorization"
}

logger?.let { response.debugLog(it, TAG) }

when (val outcome = siopOpenId4Vp.dispatch(
when (val outcome = openId4Vp.dispatch(
request = response.resolvedRequestObject,
consensus = response.vpToken,
encryptionParameters = response.encryptionParameters,
Expand Down
Loading
Loading