Skip to content

Commit

Permalink
Merge branch 'feature/eudi-interop'
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Mar 19, 2024
2 parents 853e143 + 0988635 commit f0f6fbe
Show file tree
Hide file tree
Showing 40 changed files with 805 additions and 634 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ Release NEXT:
- Fix name shadowing of gradle plugins by renaming file `Plugin.kt` -> `VcLibConventions.kt`
- Fix: Add missing iOS exports
- Add switch to disable composite build (useful for publishing)

- Get rid of arrays in serializable types and use collections instead
- Improve interoperability with verifiers and issuers from <https://github.com/eu-digital-identity-wallet/>
- `OidcSiopVerifier`: Move `credentialScheme` from constructor to `createAuthnRequest`
- `OidcSiopWallet`: Add constructor parameter to fetch JSON Web Key Sets

Release 3.4.0:
- Target Java 17
Expand Down
8 changes: 8 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ rootProject.name = "vclibrary"
include(":vclib")
include(":vclib-aries")
include(":vclib-openid")

includeBuild("kmp-crypto") {
dependencySubstitution {
substitute(module("at.asitplus.crypto:datatypes")).using(project(":datatypes"))
substitute(module("at.asitplus.crypto:datatypes-jws")).using(project(":datatypes-jws"))
substitute(module("at.asitplus.crypto:datatypes-cose")).using(project(":datatypes-cose"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -208,17 +208,17 @@ class PresentProofProtocol(
val claimsConstraints = requestedClaims?.map(this::buildConstraintFieldForClaim) ?: listOf()
val typeConstraints = buildConstraintFieldForType(credentialScheme.vcType)
val presentationDefinition = PresentationDefinition(
inputDescriptors = arrayOf(
inputDescriptors = listOf(
InputDescriptor(
name = credentialScheme.vcType,
schema = SchemaReference(uri = credentialScheme.schemaUri),
constraints = Constraint(
fields = (claimsConstraints + typeConstraints).toTypedArray()
fields = claimsConstraints + typeConstraints
)
)
),
formats = FormatHolder(
jwtVp = FormatContainerJwt(arrayOf(JwsAlgorithm.ES256.identifier))
jwtVp = FormatContainerJwt(listOf(JwsAlgorithm.ES256.identifier))
)
)
val requestPresentation = RequestPresentationAttachment(
Expand All @@ -245,12 +245,12 @@ class PresentProofProtocol(
}

private fun buildConstraintFieldForType(attributeType: String) = ConstraintField(
path = arrayOf("\$.vc[*].type", "\$.type"),
path = listOf("\$.vc[*].type", "\$.type"),
filter = ConstraintFilter(type = "string", const = attributeType)
)

private fun buildConstraintFieldForClaim(claimName: String) = ConstraintField(
path = arrayOf("\$.vc[*].name", "\$.type"),
path = listOf("\$.vc[*].name", "\$.type"),
filter = ConstraintFilter(type = "string", const = claimName)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package at.asitplus.wallet.lib.oidc

import at.asitplus.wallet.lib.data.InstantLongSerializer
import at.asitplus.wallet.lib.data.dif.PresentationDefinition
import at.asitplus.wallet.lib.oidvci.AuthorizationDetails
import io.github.aakira.napier.Napier
import kotlinx.datetime.Instant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString

/**
* Contents of an OIDC Authentication Request.
Expand Down Expand Up @@ -157,18 +161,25 @@ data class AuthenticationRequestParameters(
val clientIdScheme: String? = null,

/**
* May contain the Wallet's OIDC issuer URL, for discovery.
* Recommended in Dynamic Credential Request.
* OID4VP: OPTIONAL. String containing the Wallet's identifier. The Credential Issuer can use the discovery process
* defined in SIOPv2 to determine the Wallet's capabilities and endpoints, using the `wallet_issuer` value as the
* Issuer Identifier referred to in SIOPv2. This is RECOMMENDED in Dynamic Credential Requests.
*/
@SerialName("wallet_issuer")
val walletIssuer: String? = null,

/**
* Recommended in Dynamic Credential Request
* OID4VP: OPTIONAL. String containing an opaque End-User hint that the Wallet MAY use in subsequent callbacks to
* optimize the End-User's experience. This is RECOMMENDED in Dynamic Credential Requests.
*/
@SerialName("user_hint")
val userHint: String? = null,

/**
* OID4VP: OPTIONAL. String value identifying a certain processing context at the Credential Issuer. A value for
* this parameter is typically passed in a Credential Offer from the Credential Issuer to the Wallet. This request
* parameter is used to pass the issuer_state value back to the Credential Issuer.
*/
@SerialName("issuer_state")
val issuerState: String? = null,

Expand Down Expand Up @@ -209,4 +220,23 @@ data class AuthenticationRequestParameters(
*/
@SerialName("iss")
val issuer: String? = null,
)

/**
* OPTIONAL. Time at which the request was issued.
*/
@SerialName("iat")
@Serializable(with = InstantLongSerializer::class)
val issuedAt: Instant? = null,
) {

fun serialize() = jsonSerializer.encodeToString(this)

companion object {
fun deserialize(it: String) = kotlin.runCatching {
jsonSerializer.decodeFromString<AuthenticationRequestParameters>(it)
}.getOrElse {
Napier.w("deserialize failed", it)
null
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package at.asitplus.wallet.lib.oidc

import at.asitplus.wallet.lib.data.dif.PresentationSubmission
import io.github.aakira.napier.Napier
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString

/**
* Contents of an OIDC Authentication Response.
Expand Down Expand Up @@ -61,4 +63,16 @@ data class AuthenticationResponseParameters(
*/
@SerialName("state")
val state: String? = null,
)
) {

fun serialize() = jsonSerializer.encodeToString(this)

companion object {
fun deserialize(it: String) = kotlin.runCatching {
jsonSerializer.decodeFromString<AuthenticationResponseParameters>(it)
}.getOrElse {
Napier.w("deserialize failed", it)
null
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package at.asitplus.wallet.lib.oidc

import at.asitplus.crypto.datatypes.jws.JsonWebKey
import io.github.aakira.napier.Napier
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString

@Serializable
data class JsonWebKeySet(
@SerialName("keys")
val keys: Collection<JsonWebKey>,
) {

fun serialize() = jsonSerializer.encodeToString(this)

companion object {
fun deserialize(it: String) = kotlin.runCatching {
jsonSerializer.decodeFromString<JsonWebKeySet>(it)
}.getOrElse {
Napier.w("deserialize failed", it)
null
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ class OidcSiopVerifier(
private val verifierJwsService: VerifierJwsService,
timeLeewaySeconds: Long = 300L,
private val clock: Clock = Clock.System,
private val credentialScheme: ConstantIndex.CredentialScheme? = null,
) {

private val timeLeeway = timeLeewaySeconds.toDuration(DurationUnit.SECONDS)
Expand All @@ -77,7 +76,6 @@ class OidcSiopVerifier(
jwsService: JwsService = DefaultJwsService(cryptoService),
timeLeewaySeconds: Long = 300L,
clock: Clock = Clock.System,
credentialScheme: ConstantIndex.CredentialScheme? = null,
) = OidcSiopVerifier(
verifier = verifier,
relyingPartyUrl = relyingPartyUrl,
Expand All @@ -86,18 +84,17 @@ class OidcSiopVerifier(
verifierJwsService = verifierJwsService,
timeLeewaySeconds = timeLeewaySeconds,
clock = clock,
credentialScheme = credentialScheme,
)
}

private val containerJwt =
FormatContainerJwt(algorithms = verifierJwsService.supportedAlgorithms.map { it.identifier }.toTypedArray())
FormatContainerJwt(algorithms = verifierJwsService.supportedAlgorithms.map { it.identifier })

private val metadata by lazy {
RelyingPartyMetadata(
redirectUris = arrayOf(relyingPartyUrl),
jsonWebKeySet = JsonWebKeySet(arrayOf(agentPublicKey.toJsonWebKey())),
subjectSyntaxTypesSupported = arrayOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY),
redirectUris = listOf(relyingPartyUrl),
jsonWebKeySet = JsonWebKeySet(listOf(agentPublicKey.toJsonWebKey())),
subjectSyntaxTypesSupported = listOf(URN_TYPE_JWK_THUMBPRINT, PREFIX_DID_KEY),
vpFormats = FormatHolder(
msoMdoc = containerJwt,
jwtVp = containerJwt,
Expand Down Expand Up @@ -139,20 +136,23 @@ class OidcSiopVerifier(
*
* @param responseMode which response mode to request, see [OpenIdConstants.ResponseModes]
* @param representation specifies the required representation, see [ConstantIndex.CredentialRepresentation]
* @param credentialScheme which credential type to request, or `null` to make no restrictions
* @param requestedAttributes list of attributes that shall be requested explicitly (selective disclosure)
*/
suspend fun createAuthnRequestUrl(
walletUrl: String,
responseMode: String? = null,
representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
state: String? = uuid4().toString(),
credentialScheme: ConstantIndex.CredentialScheme? = null,
requestedAttributes: List<String>? = null,
): String {
val urlBuilder = URLBuilder(walletUrl)
createAuthnRequest(
responseMode = responseMode,
representation = representation,
state = state,
credentialScheme = credentialScheme,
requestedAttributes = requestedAttributes,
).encodeToParameters()
.forEach { urlBuilder.parameters.append(it.key, it.value) }
Expand All @@ -173,13 +173,15 @@ class OidcSiopVerifier(
responseMode: String? = null,
representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
state: String? = uuid4().toString(),
credentialScheme: ConstantIndex.CredentialScheme? = null,
requestedAttributes: List<String>? = null,
): KmmResult<String> {
val urlBuilder = URLBuilder(walletUrl)
createAuthnRequestAsRequestObject(
responseMode = responseMode,
representation = representation,
state = state,
credentialScheme = credentialScheme,
requestedAttributes = requestedAttributes,
).getOrElse {
return KmmResult.failure(it)
Expand All @@ -194,18 +196,21 @@ class OidcSiopVerifier(
* @param responseMode which response mode to request, see [OpenIdConstants.ResponseModes]
* @param representation specifies the required representation, see [ConstantIndex.CredentialRepresentation]
* @param state opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult]
* @param credentialScheme which credential type to request, or `null` to make no restrictions
* @param requestedAttributes list of attributes that shall be requested explicitly (selective disclosure)
*/
suspend fun createAuthnRequestAsRequestObject(
responseMode: String? = null,
representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
state: String? = uuid4().toString(),
credentialScheme: ConstantIndex.CredentialScheme? = null,
requestedAttributes: List<String>? = null,
): KmmResult<AuthenticationRequestParameters> {
val requestObject = createAuthnRequest(
responseMode = responseMode,
representation = representation,
state = state,
credentialScheme = credentialScheme,
requestedAttributes = requestedAttributes,
)
val requestObjectSerialized = jsonSerializer.encodeToString(
Expand Down Expand Up @@ -235,12 +240,14 @@ class OidcSiopVerifier(
* @param responseMode which response mode to request, see [OpenIdConstants.ResponseModes]
* @param representation specifies the required representation, see [ConstantIndex.CredentialRepresentation]
* @param state opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult]
* @param credentialScheme which credential type to request, or `null` to make no restrictions
* @param requestedAttributes list of attributes that shall be requested explicitly (selective disclosure)
*/
suspend fun createAuthnRequest(
responseMode: String? = null,
representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
state: String? = uuid4().toString(),
credentialScheme: ConstantIndex.CredentialScheme? = null,
requestedAttributes: List<String>? = null,
): AuthenticationRequestParameters {
val typeConstraint = credentialScheme?.let {
Expand All @@ -250,8 +257,8 @@ class OidcSiopVerifier(
ConstantIndex.CredentialRepresentation.ISO_MDOC -> it.isoConstraint()
}
}
val attributeConstraint = requestedAttributes?.let { createConstraints(representation, it) } ?: arrayOf()
val constraintFields = listOfNotNull(typeConstraint, *attributeConstraint).toTypedArray()
val attributeConstraint = requestedAttributes?.let { createConstraints(representation, it) } ?: listOf()
val constraintFields = attributeConstraint + typeConstraint
return AuthenticationRequestParameters(
responseType = "$ID_TOKEN $VP_TOKEN",
clientId = relyingPartyUrl,
Expand All @@ -266,11 +273,11 @@ class OidcSiopVerifier(
presentationDefinition = PresentationDefinition(
id = uuid4().toString(),
formats = representation.toFormatHolder(),
inputDescriptors = arrayOf(
inputDescriptors = listOf(
InputDescriptor(
id = uuid4().toString(),
schema = arrayOf(SchemaReference(credentialScheme?.schemaUri ?: "https://example.com")),
constraints = Constraint(fields = constraintFields),
schema = listOf(SchemaReference(credentialScheme?.schemaUri ?: "https://example.com")),
constraints = Constraint(fields = constraintFields.filterNotNull()),
)
),
),
Expand All @@ -284,15 +291,15 @@ class OidcSiopVerifier(
}

private fun ConstantIndex.CredentialScheme.vcConstraint() = ConstraintField(
path = arrayOf("$.type"),
path = listOf("$.type"),
filter = ConstraintFilter(
type = "string",
pattern = vcType,
)
)

private fun ConstantIndex.CredentialScheme.isoConstraint() = ConstraintField(
path = arrayOf("$.mdoc.doctype"),
path = listOf("$.mdoc.doctype"),
filter = ConstraintFilter(
type = "string",
pattern = isoDocType,
Expand All @@ -302,12 +309,12 @@ class OidcSiopVerifier(
private fun createConstraints(
credentialRepresentation: ConstantIndex.CredentialRepresentation,
attributeTypes: List<String>,
): Array<ConstraintField> = attributeTypes.map {
): Collection<ConstraintField> = attributeTypes.map {
if (credentialRepresentation == ConstantIndex.CredentialRepresentation.ISO_MDOC)
ConstraintField(path = arrayOf("\$.mdoc.$it"), intentToRetain = false)
ConstraintField(path = listOf("\$.mdoc.$it"), intentToRetain = false)
else
ConstraintField(path = arrayOf("\$.$it"))
}.toTypedArray()
ConstraintField(path = listOf("\$.$it"))
}


sealed class AuthnResponseResult {
Expand Down Expand Up @@ -410,7 +417,7 @@ class OidcSiopVerifier(
val presentationSubmission = params.presentationSubmission
?: return AuthnResponseResult.ValidationError("presentation_submission", params.state)
.also { Napier.w("presentation_submission empty") }
val descriptor = presentationSubmission.descriptorMap?.get(0)
val descriptor = presentationSubmission.descriptorMap?.firstOrNull()
?: return AuthnResponseResult.ValidationError("presentation_submission", params.state)
.also { Napier.w("presentation_submission contains no descriptors") }
val vp = params.vpToken
Expand Down
Loading

0 comments on commit f0f6fbe

Please sign in to comment.