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

SIOP: Multiple VP Tokens #138

Merged
merged 6 commits into from
Sep 27, 2024
Merged
Show file tree
Hide file tree
Changes from 5 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 @@ -39,6 +39,10 @@ Release NEXT:
- Introudce `OAuth2Client` to extract creating authentication requests and token requests from OID4VCI `WalletService`
- Refactor `SimpleAuthorizationService` to extract actual authentication and authorization into `AuthorizationServiceStrategy`
- Implement JWE encryption with AES-CBC-HMAC algorithms
- SIOPv2/OpenID4VP: Support requesting and receiving claims from different credentials, i.e. a combined presentation
- Require request options on every method in `OidcSiopVerifier`
- Move `credentialScheme`, `representation`, `requestedAttributes` from `RequestOptions` to `RequestOptionsCredentials`
- In `OidcSiopVerifier` move `responseUrl` from constructor parameter to `RequestOptions`

Release 4.1.2:
* In `OidcSiopVerifier` add parameter `nonceService` to externalize creation and validation of nonces, e.g. for deployments in load-balanced environments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ data class PresentationDefinition(
val purpose: String? = null,
@SerialName("input_descriptors")
val inputDescriptors: Collection<InputDescriptor>,
@Deprecated(message = "Removed in DIF Presentation Exchange 2.0.0", ReplaceWith("inputDescriptors.format"))
@SerialName("format")
val formats: FormatHolder? = null,
@SerialName("submission_requirements")
val submissionRequirements: Collection<SubmissionRequirement>? = null,
) {
@Deprecated(message = "Removed in DIF Presentation Exchange 2.0.0")
constructor(
inputDescriptors: Collection<InputDescriptor>,
formats: FormatHolder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
package at.asitplus.wallet.lib.aries

import at.asitplus.dif.*
import at.asitplus.signum.indispensable.josef.JsonWebKey
import at.asitplus.signum.indispensable.josef.JwsAlgorithm
import at.asitplus.wallet.lib.agent.Holder
import at.asitplus.wallet.lib.agent.Verifier
import at.asitplus.wallet.lib.data.AriesGoalCodeParser
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.SchemaIndex
import at.asitplus.dif.Constraint
import at.asitplus.dif.ConstraintField
import at.asitplus.dif.ConstraintFilter
import at.asitplus.dif.DifInputDescriptor
import at.asitplus.dif.FormatContainerJwt
import at.asitplus.dif.FormatHolder
import at.asitplus.dif.PresentationDefinition
import at.asitplus.wallet.lib.msg.AttachmentFormatReference
import at.asitplus.wallet.lib.msg.JsonWebMessage
import at.asitplus.wallet.lib.msg.JwmAttachment
import at.asitplus.wallet.lib.msg.OutOfBandInvitation
import at.asitplus.wallet.lib.msg.OutOfBandInvitationBody
import at.asitplus.wallet.lib.msg.OutOfBandService
import at.asitplus.wallet.lib.msg.Presentation
import at.asitplus.wallet.lib.msg.PresentationBody
import at.asitplus.wallet.lib.msg.RequestPresentation
import at.asitplus.wallet.lib.msg.RequestPresentationAttachment
import at.asitplus.wallet.lib.msg.RequestPresentationAttachmentOptions
import at.asitplus.wallet.lib.msg.RequestPresentationBody
import at.asitplus.wallet.lib.msg.*
import com.benasher44.uuid.uuid4
import io.github.aakira.napier.Napier
import kotlinx.serialization.encodeToString
Expand Down Expand Up @@ -203,6 +186,7 @@ class PresentProofProtocol(
.also { this.state = State.REQUEST_PRESENTATION_SENT }
}

@Suppress("DEPRECATION")
private fun buildRequestPresentationMessage(
credentialScheme: ConstantIndex.CredentialScheme,
parentThreadId: String? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ import kotlin.time.toDuration
class OidcSiopVerifier private constructor(
private val verifier: Verifier,
private val relyingPartyUrl: String?,
private val responseUrl: String?,
private val jwsService: JwsService,
private val verifierJwsService: VerifierJwsService,
timeLeewaySeconds: Long = 300L,
Expand Down Expand Up @@ -83,7 +82,6 @@ class OidcSiopVerifier private constructor(
keyMaterial: KeyMaterial = EphemeralKeyWithoutCert(),
verifier: Verifier = VerifierAgent(keyMaterial),
relyingPartyUrl: String? = null,
responseUrl: String? = null,
verifierJwsService: VerifierJwsService = DefaultVerifierJwsService(DefaultVerifierCryptoService()),
jwsService: JwsService = DefaultJwsService(DefaultCryptoService(keyMaterial)),
timeLeewaySeconds: Long = 300L,
Expand All @@ -93,7 +91,6 @@ class OidcSiopVerifier private constructor(
) : this(
verifier = verifier,
relyingPartyUrl = relyingPartyUrl,
responseUrl = responseUrl,
jwsService = jwsService,
verifierJwsService = verifierJwsService,
timeLeewaySeconds = timeLeewaySeconds,
Expand Down Expand Up @@ -161,43 +158,64 @@ class OidcSiopVerifier private constructor(

data class RequestOptions(
/**
* Response mode to request, see [OpenIdConstants.ResponseMode]
* Requested credentials, should be at least one
*/
val responseMode: OpenIdConstants.ResponseMode? = null,
val credentials: Set<RequestOptionsCredential>,
/**
* Required representation, see [ConstantIndex.CredentialRepresentation]
* Response mode to request, see [OpenIdConstants.ResponseMode],
* by default [OpenIdConstants.ResponseMode.FRAGMENT].
* Setting this to any other value may require setting [responseUrl] too.
*/
val representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
val responseMode: OpenIdConstants.ResponseMode = OpenIdConstants.ResponseMode.FRAGMENT,
/**
* Opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult]
* Response URL to set in the [AuthenticationRequestParameters.responseUrl],
* required if [responseMode] is set to [OpenIdConstants.ResponseMode.DIRECT_POST] or
* [OpenIdConstants.ResponseMode.DIRECT_POST_JWT].
*/
val state: String? = uuid4().toString(),
val responseUrl: String? = null,
/**
* Credential type to request, or `null` to make no restrictions
* Response type to set in [AuthenticationRequestParameters.responseType],
* by default only `vp_token` (as per OpenID4VP spec).
* Be sure to separate values by a space, e.g. `vp_token id_token`.
*/
val credentialScheme: ConstantIndex.CredentialScheme? = null,
val responseType: String = VP_TOKEN,
/**
* List of attributes that shall be requested explicitly (selective disclosure),
* or `null` to make no restrictions
* Opaque value which will be returned by the OpenId Provider and also in [AuthnResponseResult]
*/
val requestedAttributes: List<String>? = null,
val state: String = uuid4().toString(),
/**
* Optional URL to include [metadata] by reference instead of by value (directly embedding in authn request)
*/
val clientMetadataUrl: String? = null,
/**
* Set this value to include metadata with encryption parameters set. Beware if setting this value and also
* [clientMetadataUrl], that the URL shall point to [getCreateMetadataWithEncryption].
* [clientMetadataUrl], that the URL shall point to [OidcSiopVerifier.metadataWithEncryption].
*/
val encryption: Boolean = false,
)

data class RequestOptionsCredential(
/**
* Credential type to request, or `null` to make no restrictions
*/
val credentialScheme: ConstantIndex.CredentialScheme,
/**
* Required representation, see [ConstantIndex.CredentialRepresentation]
*/
val representation: ConstantIndex.CredentialRepresentation = ConstantIndex.CredentialRepresentation.PLAIN_JWT,
/**
* List of attributes that shall be requested explicitly (selective disclosure),
* or `null` to make no restrictions
*/
val requestedAttributes: List<String>? = null,
)

/**
* Creates an OIDC Authentication Request, encoded as query parameters to the [walletUrl].
*/
suspend fun createAuthnRequestUrl(
walletUrl: String,
requestOptions: RequestOptions = RequestOptions(),
requestOptions: RequestOptions,
): String {
val urlBuilder = URLBuilder(walletUrl)
createAuthnRequest(requestOptions).encodeToParameters()
Expand All @@ -211,7 +229,7 @@ class OidcSiopVerifier private constructor(
*/
suspend fun createAuthnRequestUrlWithRequestObject(
walletUrl: String,
requestOptions: RequestOptions = RequestOptions()
requestOptions: RequestOptions,
): KmmResult<String> = catching {
val jar = createAuthnRequestAsSignedRequestObject(requestOptions).getOrThrow()
val urlBuilder = URLBuilder(walletUrl)
Expand All @@ -234,7 +252,7 @@ class OidcSiopVerifier private constructor(
suspend fun createAuthnRequestUrlWithRequestObjectByReference(
walletUrl: String,
requestUrl: String,
requestOptions: RequestOptions = RequestOptions()
requestOptions: RequestOptions,
): KmmResult<Pair<String, String>> = catching {
val jar = createAuthnRequestAsSignedRequestObject(requestOptions).getOrThrow()
val urlBuilder = URLBuilder(walletUrl)
Expand All @@ -260,7 +278,7 @@ class OidcSiopVerifier private constructor(
* ```
*/
suspend fun createAuthnRequestAsSignedRequestObject(
requestOptions: RequestOptions = RequestOptions(),
requestOptions: RequestOptions,
): KmmResult<JwsSigned> = catching {
val requestObject = createAuthnRequest(requestOptions)
val requestObjectSerialized = jsonSerializer.encodeToString(
Expand All @@ -286,38 +304,38 @@ class OidcSiopVerifier private constructor(
* Callers may serialize the result with `result.encodeToParameters().formUrlEncode()`
*/
suspend fun createAuthnRequest(
requestOptions: RequestOptions = RequestOptions(),
requestOptions: RequestOptions,
) = AuthenticationRequestParameters(
responseType = "$ID_TOKEN $VP_TOKEN",
responseType = requestOptions.responseType,
clientId = clientId,
redirectUrl = requestOptions.buildRedirectUrl(),
responseUrl = responseUrl,
responseUrl = requestOptions.responseUrl,
clientIdScheme = clientIdScheme.clientIdScheme,
scope = requestOptions.buildScope(),
nonce = nonceService.provideNonce(),
clientMetadata = requestOptions.clientMetadataUrl?.let { null }
?: if (requestOptions.encryption) metadataWithEncryption else metadata,
clientMetadata = if (requestOptions.clientMetadataUrl != null) {
null
} else {
if (requestOptions.encryption) metadataWithEncryption else metadata
},
clientMetadataUri = requestOptions.clientMetadataUrl,
idTokenType = IdTokenType.SUBJECT_SIGNED.text,
responseMode = requestOptions.responseMode,
state = requestOptions.state,
presentationDefinition = PresentationDefinition(
id = uuid4().toString(),
formats = requestOptions.representation.toFormatHolder(),
inputDescriptors = listOf(
requestOptions.toInputDescriptor()
),
inputDescriptors = requestOptions.credentials.map {
it.toInputDescriptor()
},
),
)

private fun RequestOptions.buildScope() = listOfNotNull(
SCOPE_OPENID,
SCOPE_PROFILE,
credentialScheme?.sdJwtType,
credentialScheme?.vcType,
credentialScheme?.isoNamespace
)
.joinToString(" ")
private fun RequestOptions.buildScope() = (
listOf(SCOPE_OPENID, SCOPE_PROFILE)
+ credentials.mapNotNull { it.credentialScheme.sdJwtType }
+ credentials.mapNotNull { it.credentialScheme.vcType }
+ credentials.mapNotNull { it.credentialScheme.isoNamespace }
).joinToString(" ")

private val clientId: String? by lazy {
clientIdFromCertificateChain ?: relyingPartyUrl
Expand All @@ -333,8 +351,9 @@ class OidcSiopVerifier private constructor(
) null else relyingPartyUrl

//TODO extend for InputDescriptor interface in case QES
private fun RequestOptions.toInputDescriptor() = DifInputDescriptor(
private fun RequestOptionsCredential.toInputDescriptor() = DifInputDescriptor(
id = buildId(),
format = toFormatHolder(),
constraints = toConstraint(),
)

Expand All @@ -343,26 +362,24 @@ class OidcSiopVerifier private constructor(
* encoding it into the descriptor id as in the following non-normative example fow now:
* https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#appendix-A.3.1-4
*/
private fun RequestOptions.buildId() =
if (credentialScheme?.isoDocType != null && representation == ConstantIndex.CredentialRepresentation.ISO_MDOC)
private fun RequestOptionsCredential.buildId() =
if (credentialScheme.isoDocType != null && representation == ConstantIndex.CredentialRepresentation.ISO_MDOC)
credentialScheme.isoDocType!! else uuid4().toString()

private fun RequestOptions.toConstraint() =
private fun RequestOptionsCredential.toConstraint() =
Constraint(fields = (toAttributeConstraints() + toTypeConstraint()).filterNotNull())

private fun RequestOptions.toAttributeConstraints() =
private fun RequestOptionsCredential.toAttributeConstraints() =
requestedAttributes?.createConstraints(representation, credentialScheme)
?: listOf()

private fun RequestOptions.toTypeConstraint() = credentialScheme?.let {
when (representation) {
ConstantIndex.CredentialRepresentation.PLAIN_JWT -> it.toVcConstraint()
ConstantIndex.CredentialRepresentation.SD_JWT -> it.toSdJwtConstraint()
ConstantIndex.CredentialRepresentation.ISO_MDOC -> null
}
private fun RequestOptionsCredential.toTypeConstraint() = when (representation) {
ConstantIndex.CredentialRepresentation.PLAIN_JWT -> this.credentialScheme.toVcConstraint()
ConstantIndex.CredentialRepresentation.SD_JWT -> this.credentialScheme.toSdJwtConstraint()
ConstantIndex.CredentialRepresentation.ISO_MDOC -> null
}

private fun ConstantIndex.CredentialRepresentation.toFormatHolder() = when (this) {
private fun RequestOptionsCredential.toFormatHolder() = when (representation) {
ConstantIndex.CredentialRepresentation.PLAIN_JWT -> FormatHolder(jwtVp = containerJwt)
ConstantIndex.CredentialRepresentation.SD_JWT -> FormatHolder(jwtSd = containerJwt)
ConstantIndex.CredentialRepresentation.ISO_MDOC -> FormatHolder(msoMdoc = containerJwt)
Expand All @@ -387,10 +404,10 @@ class OidcSiopVerifier private constructor(
) else null

private fun List<String>.createConstraints(
credentialRepresentation: ConstantIndex.CredentialRepresentation,
representation: ConstantIndex.CredentialRepresentation,
credentialScheme: ConstantIndex.CredentialScheme?,
): Collection<ConstraintField> = map {
if (credentialRepresentation == ConstantIndex.CredentialRepresentation.ISO_MDOC)
if (representation == ConstantIndex.CredentialRepresentation.ISO_MDOC)
credentialScheme.toConstraintField(it)
else
ConstraintField(path = listOf("\$[${it.quote()}]"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,24 @@ package at.asitplus.wallet.lib.oidc.helper

import at.asitplus.KmmResult
import at.asitplus.catching
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.josef.JwsSigned
import at.asitplus.signum.indispensable.josef.toJsonWebKey
import at.asitplus.wallet.lib.agent.CredentialSubmission
import at.asitplus.wallet.lib.agent.Holder
import at.asitplus.wallet.lib.agent.toDefaultSubmission
import at.asitplus.dif.ClaimFormatEnum
import at.asitplus.dif.FormatHolder
import at.asitplus.dif.PresentationDefinition
import at.asitplus.wallet.lib.data.dif.PresentationSubmissionValidator
import at.asitplus.wallet.lib.jws.JwsService
import at.asitplus.openid.AuthenticationRequestParameters
import at.asitplus.wallet.lib.oidc.AuthenticationRequestParametersFrom
import at.asitplus.openid.IdToken
import at.asitplus.openid.OpenIdConstants.Errors
import at.asitplus.openid.OpenIdConstants.ID_TOKEN
import at.asitplus.openid.OpenIdConstants.VP_TOKEN
import at.asitplus.openid.RelyingPartyMetadata
import at.asitplus.signum.indispensable.CryptoPublicKey
import at.asitplus.signum.indispensable.josef.JwsSigned
import at.asitplus.signum.indispensable.josef.toJsonWebKey
import at.asitplus.wallet.lib.agent.CredentialSubmission
import at.asitplus.wallet.lib.agent.Holder
import at.asitplus.wallet.lib.agent.toDefaultSubmission
import at.asitplus.wallet.lib.data.dif.PresentationSubmissionValidator
import at.asitplus.wallet.lib.jws.JwsService
import at.asitplus.wallet.lib.oidc.AuthenticationRequestParametersFrom
import at.asitplus.wallet.lib.oidvci.OAuth2Exception
import io.github.aakira.napier.Napier
import kotlinx.datetime.Clock
Expand All @@ -44,7 +44,7 @@ internal class PresentationFactory(
val credentialSubmissions = inputDescriptorSubmissions
?: holder.matchInputDescriptorsAgainstCredentialStore(
inputDescriptors = presentationDefinition.inputDescriptors,
fallbackFormatHolder = presentationDefinition.formats ?: clientMetadata?.vpFormats,
fallbackFormatHolder = clientMetadata?.vpFormats,
).getOrThrow().toDefaultSubmission()

presentationDefinition.validateSubmission(
Expand Down Expand Up @@ -129,9 +129,9 @@ internal class PresentationFactory(
}

val constraintFieldMatches = holder.evaluateInputDescriptorAgainstCredential(
inputDescriptor,
submission.value.credential,
fallbackFormatHolder = this.formats ?: clientMetadata?.vpFormats,
inputDescriptor = inputDescriptor,
credential = submission.value.credential,
fallbackFormatHolder = clientMetadata?.vpFormats,
pathAuthorizationValidator = { true },
).getOrThrow()

Expand Down
Loading
Loading