Skip to content

Commit

Permalink
Work on EUDI interoperability
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Mar 13, 2024
1 parent daf4199 commit 8bf1b24
Show file tree
Hide file tree
Showing 8 changed files with 480 additions and 87 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Release NEXT:
- 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/>

Release 3.4.0:
- Target Java 17
Expand Down
2 changes: 1 addition & 1 deletion kmp-crypto
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 @@ -2,7 +2,6 @@ package at.asitplus.wallet.lib.oidc

import at.asitplus.KmmResult
import at.asitplus.crypto.datatypes.CryptoPublicKey
import at.asitplus.crypto.datatypes.jws.JwsAlgorithm
import at.asitplus.crypto.datatypes.jws.JwsSigned
import at.asitplus.crypto.datatypes.jws.toJsonWebKey
import at.asitplus.wallet.lib.agent.CryptoService
Expand Down Expand Up @@ -53,7 +52,13 @@ class OidcSiopWallet(
private val jwsService: JwsService,
private val verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(),
private val clock: Clock = Clock.System,
private val clientId: String = "https://wallet.a-sit.at/"
private val clientId: String = "https://wallet.a-sit.at/",
/**
* Need to implement if JSON web keys are not specified directly as `jwks` in authn requests,
* but need to be retrieved from the `jwks_uri`. Simply fetch the URL passed and return the content
* parsed as [JsonWebKeySet].
*/
private val jwkSetRetriever: (String) -> JsonWebKeySet? = { null }
) {

companion object {
Expand All @@ -63,14 +68,16 @@ class OidcSiopWallet(
jwsService: JwsService = DefaultJwsService(cryptoService),
verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(),
clock: Clock = Clock.System,
clientId: String = "https://wallet.a-sit.at/"
clientId: String = "https://wallet.a-sit.at/",
jwkSetRetriever: (String) -> JsonWebKeySet? = { null }
) = OidcSiopWallet(
holder = holder,
agentPublicKey = cryptoService.publicKey,
jwsService = jwsService,
verifierJwsService = verifierJwsService,
clock = clock,
clientId = clientId,
jwkSetRetriever = jwkSetRetriever
)
}

Expand Down Expand Up @@ -133,7 +140,7 @@ class OidcSiopWallet(
JwsSigned.parse(requestObject)?.let { jws ->
if (verifierJwsService.verifyJwsObject(jws)) {
return kotlin.runCatching {
jsonSerializer.decodeFromString<AuthenticationRequestParameters>(jws.payload.decodeToString())
AuthenticationRequestParameters.deserialize(jws.payload.decodeToString())
}.getOrNull()
}
}
Expand All @@ -154,17 +161,17 @@ class OidcSiopWallet(
if (!request.responseType.contains(ID_TOKEN) && !request.responseType.contains(VP_TOKEN)) {
return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST))
}
if (request.responseMode?.contains(POST) == true) {
if (request.responseMode?.startsWith(POST) == true) {
if (request.redirectUrl == null)
return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST))
val body = responseParams.encodeToParameters().formUrlEncode()
KmmResult.success(AuthenticationResponseResult.Post(request.redirectUrl, body))
} else if (request.responseMode?.contains(DIRECT_POST) == true) {
return KmmResult.success(AuthenticationResponseResult.Post(request.redirectUrl, body))
} else if (request.responseMode?.startsWith(DIRECT_POST) == true) {
if (request.responseUrl == null || request.redirectUrl != null)
return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST))
val body = responseParams.encodeToParameters().formUrlEncode()
KmmResult.success(AuthenticationResponseResult.Post(request.responseUrl, body))
} else if (request.responseMode?.contains(QUERY) == true) {
return KmmResult.success(AuthenticationResponseResult.Post(request.responseUrl, body))
} else if (request.responseMode?.startsWith(QUERY) == true) {
if (request.redirectUrl == null)
return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST))
val url = URLBuilder(request.redirectUrl)
Expand All @@ -174,15 +181,15 @@ class OidcSiopWallet(
}
}
.buildString()
KmmResult.success(AuthenticationResponseResult.Redirect(url))
return KmmResult.success(AuthenticationResponseResult.Redirect(url))
} else {
// default for vp_token and id_token is fragment
if (request.redirectUrl == null)
return KmmResult.failure(OAuth2Exception(Errors.INVALID_REQUEST))
val url = URLBuilder(request.redirectUrl)
.apply { encodedFragment = responseParams.encodeToParameters().formUrlEncode() }
.buildString()
KmmResult.success(AuthenticationResponseResult.Redirect(url))
return KmmResult.success(AuthenticationResponseResult.Redirect(url))
}
}, {
return KmmResult.failure(it)
Expand All @@ -195,34 +202,49 @@ class OidcSiopWallet(
suspend fun createAuthnResponseParams(
params: AuthenticationRequestParameters
): KmmResult<AuthenticationResponseParameters> {
val audience = params.clientMetadata?.jsonWebKeySet?.keys?.firstOrNull()?.identifier
if (params.clientMetadata == null) {
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.INVALID_REQUEST))
.also { Napier.w("client metadata is not specified") }
}
val audience = params.clientMetadata.jsonWebKeySet?.keys?.firstOrNull()?.identifier
?: params.clientMetadata.jsonWebKeySetUrl?.let { jwkSetRetriever.invoke(it)?.keys?.firstOrNull()?.identifier }
?: return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.INVALID_REQUEST))
.also { Napier.w("Could not parse audience") }
if (URN_TYPE_JWK_THUMBPRINT !in params.clientMetadata.subjectSyntaxTypesSupported)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.SUBJECT_SYNTAX_TYPES_NOT_SUPPORTED))
.also { Napier.w("Incompatible subject syntax types algorithms") }
if (params.clientId != params.redirectUrl)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.INVALID_REQUEST))
.also { Napier.w("client_id does not match redirect_uri") }
if (params.responseType?.contains(ID_TOKEN) != true)
if (params.redirectUrl != null) {
// TODO is this from eudi verifier correctly set, that the redirect_uri is not set?
if (params.clientId != params.redirectUrl)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.INVALID_REQUEST))
.also { Napier.w("client_id does not match redirect_uri") }
}
// TODO EUDI Verifier doesn't set the value
//if (params.responseType?.contains(ID_TOKEN) != true)
// return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.INVALID_REQUEST))
// .also { Napier.w("response_type is not \"$ID_TOKEN\"") }
if (params.responseType == null)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.INVALID_REQUEST))
.also { Napier.w("response_type is not \"$ID_TOKEN\"") }
.also { Napier.w("response_type is not specified") }
if (!params.responseType.contains(VP_TOKEN) && params.presentationDefinition == null)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.INVALID_REQUEST))
.also { Napier.w("vp_token not requested") }
// TODO Client shall send the client_id_scheme, which needs to be supported by the Wallet
if (params.clientMetadata.vpFormats == null)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED))
.also { Napier.w("Incompatible subject syntax types algorithms") }
if (params.clientMetadata.vpFormats.jwtVp?.algorithms?.contains(jwsService.algorithm.identifier) != true)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED))
.also { Napier.w("Incompatible JWT algorithms") }
if (params.clientMetadata.vpFormats.jwtSd?.algorithms?.contains(jwsService.algorithm.identifier) != true)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED))
.also { Napier.w("Incompatible JWT algorithms") }
if (params.clientMetadata.vpFormats.msoMdoc?.algorithms?.contains(jwsService.algorithm.identifier) != true)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED))
.also { Napier.w("Incompatible JWT algorithms") }
// TODO EUDI Verifier doesn't set the value
//if (params.clientMetadata.vpFormats == null)
// return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED))
// .also { Napier.w("Incompatible subject syntax types algorithms") }
if (params.clientMetadata.vpFormats != null) {
if (params.clientMetadata.vpFormats.jwtVp?.algorithms?.contains(jwsService.algorithm.identifier) != true)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED))
.also { Napier.w("Incompatible JWT algorithms") }
if (params.clientMetadata.vpFormats.jwtSd?.algorithms?.contains(jwsService.algorithm.identifier) != true)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED))
.also { Napier.w("Incompatible JWT algorithms") }
if (params.clientMetadata.vpFormats.msoMdoc?.algorithms?.contains(jwsService.algorithm.identifier) != true)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.REGISTRATION_VALUE_NOT_SUPPORTED))
.also { Napier.w("Incompatible JWT algorithms") }
}
if (params.nonce == null)
return KmmResult.failure<AuthenticationResponseParameters>(OAuth2Exception(Errors.INVALID_REQUEST))
.also { Napier.w("nonce is null") }
Expand All @@ -233,7 +255,7 @@ class OidcSiopWallet(
issuer = agentPublicKey.toJsonWebKey().jwkThumbprint,
subject = agentPublicKey.toJsonWebKey().jwkThumbprint,
subjectJwk = agentPublicKey.toJsonWebKey(),
audience = params.redirectUrl,
audience = params.redirectUrl ?: params.clientId,
issuedAt = now,
expiration = now + 60.seconds,
nonce = params.nonce,
Expand Down Expand Up @@ -338,6 +360,4 @@ class OidcSiopWallet(

}
}


}
}
Loading

0 comments on commit 8bf1b24

Please sign in to comment.