Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

EUDI Interop #41

Merged
merged 6 commits into from
Mar 19, 2024
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: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Release NEXT:
- Given that all EC keys were previously uncompressed, different mutlicodec identifiers are now supported and the old encoding of uncompressed keys does not work anymore, as it was faulty.
- In addition, the encoding of the mutlibase prefix has changed, since varint-Encoding is now used correctly.
- Fix name shadowing of gradle plugins by renaming file `Plugin.kt` -> `VcLibConventions.kt`
- 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
2 changes: 1 addition & 1 deletion kmp-crypto
8 changes: 8 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,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
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
Loading