From de4c82dc3bf9ce5a4801f471471a3ffba7803521 Mon Sep 17 00:00:00 2001 From: Dimitris ZARRAS Date: Thu, 26 Sep 2024 11:36:00 +0300 Subject: [PATCH] Final bits for batch credential issuance. --- README.md | 8 ++++++ .../ec/eudi/pidissuer/PidIssuerApplication.kt | 9 +++++++ .../domain/CredentialIssuerMetaData.kt | 26 ++++++++++++++++--- .../port/input/GetCredentialIssuerMetaData.kt | 14 +++++++--- .../pidissuer/port/input/IssueCredential.kt | 26 ++++++++++++++++--- src/main/resources/application.properties | 2 ++ 6 files changed, 75 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 4810a27..6559ab4 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,14 @@ Variable: `ISSUER_DPOP_REALM` Description: Realm to report in the WWW-Authenticate header in case of DPoP authentication/authorization failure Default value: `pid-issuer` +Variable: `ISSUER_CREDENTIALENDPOINT_BATCHISSUANCE_ENABLED` +Description: Whether to enable batch issuance support in the credential endpoint +Default value: `true` + +Variable: `ISSUER_CREDENTIALENDPOINT_BATCHISSUANCE_BATCHSIZE` +Description: Maximum length of `proofs` array supported by credential endpoint when batch issuance support is enabled +Default value: `10` + ### Signing Key When either PID issuance in SD-JWT is enabled, or the internal MSO MDoc encoder is used, an EC Key is required diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt index 7b18fd9..ff1bb6c 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/PidIssuerApplication.kt @@ -442,6 +442,15 @@ fun beans(clock: Clock) = beans { add(mdlIssuer) } }, + batchCredentialIssuance = run { + val enabled = env.getProperty("issuer.credentialEndpoint.batchIssuance.enabled") ?: true + if (enabled) { + val batchSize = env.getProperty("issuer.credentialEndpoint.batchIssuance.batchSize") ?: 10 + BatchCredentialIssuance.Supported(batchSize) + } else { + BatchCredentialIssuance.NotSupported + } + }, ) } diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialIssuerMetaData.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialIssuerMetaData.kt index f3ab7df..b04e9b2 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialIssuerMetaData.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/domain/CredentialIssuerMetaData.kt @@ -90,9 +90,7 @@ data class CredentialIssuerDisplay( * @param credentialEndPoint URL of the Credential Issuer's Credential Endpoint. * This URL MUST use the https scheme and MAY contain port, path, * and query parameter components - * @param batchCredentialEndpoint URL of the Credential Issuer's Batch Credential Endpoint. - * This URL MUST use the https scheme and MAY contain port, path, and query parameter components. - * If omitted, the Credential Issuer does not support the Batch Credential Endpoint + * @param batchCredentialIssuance whether the credential endpoint supports batch issuance or not * @param deferredCredentialEndpoint URL of the Credential Issuer's * Deferred Credential Endpoint. This URL MUST use the https scheme and MAY contain port, path, * and query parameter components. @@ -109,7 +107,7 @@ data class CredentialIssuerMetaData( val id: CredentialIssuerId, val authorizationServers: List, val credentialEndPoint: HttpsUrl, - val batchCredentialEndpoint: HttpsUrl? = null, + val batchCredentialIssuance: BatchCredentialIssuance, val deferredCredentialEndpoint: HttpsUrl? = null, val notificationEndpoint: HttpsUrl? = null, val credentialResponseEncryption: CredentialResponseEncryption, @@ -119,3 +117,23 @@ data class CredentialIssuerMetaData( val credentialConfigurationsSupported: List get() = specificCredentialIssuers.map { it.supportedCredential } } + +/** + * Indicates whether the Credential Endpoint can support batch issuance or not. + */ +sealed interface BatchCredentialIssuance { + + /** + * Batch credential issuance is not supported. + */ + data object NotSupported : BatchCredentialIssuance + + /** + * Batch credential issuance is supported. + */ + data class Supported(val batchSize: Int) : BatchCredentialIssuance { + init { + require(batchSize > 0) { "Batch size must be greater than 0" } + } + } +} diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetCredentialIssuerMetaData.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetCredentialIssuerMetaData.kt index 784d55f..4a1afab 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetCredentialIssuerMetaData.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/GetCredentialIssuerMetaData.kt @@ -37,14 +37,14 @@ data class CredentialIssuerMetaDataTO( val authorizationServers: List? = null, @Required @SerialName("credential_endpoint") val credentialEndpoint: String, - @SerialName("batch_credential_endpoint") - val batchCredentialEndpoint: String? = null, @SerialName("deferred_credential_endpoint") val deferredCredentialEndpoint: String? = null, @SerialName("notification_endpoint") val notificationEndpoint: String? = null, @SerialName("credential_response_encryption") val credentialResponseEncryption: CredentialResponseEncryptionTO? = null, + @SerialName("batch_credential_issuance") + val batchCredentialIssuance: BatchCredentialIssuanceTO? = null, @SerialName("credential_identifiers_supported") val credentialIdentifiersSupported: Boolean? = null, @SerialName("signed_metadata") @@ -64,6 +64,11 @@ data class CredentialIssuerMetaDataTO( @Required @SerialName("encryption_required") val required: Boolean, ) + + @Serializable + data class BatchCredentialIssuanceTO( + @Required @SerialName("batch_size") val batchSize: Int, + ) } @Serializable @@ -88,10 +93,13 @@ private fun CredentialIssuerMetaData.toTransferObject(): CredentialIssuerMetaDat credentialIssuer = id.externalForm, authorizationServers = authorizationServers.map { it.externalForm }, credentialEndpoint = credentialEndPoint.externalForm, - batchCredentialEndpoint = batchCredentialEndpoint?.externalForm, deferredCredentialEndpoint = deferredCredentialEndpoint?.externalForm, notificationEndpoint = notificationEndpoint?.externalForm, credentialResponseEncryption = credentialResponseEncryption.toTransferObject().getOrNull(), + batchCredentialIssuance = when (batchCredentialIssuance) { + BatchCredentialIssuance.NotSupported -> null + is BatchCredentialIssuance.Supported -> CredentialIssuerMetaDataTO.BatchCredentialIssuanceTO(batchCredentialIssuance.batchSize) + }, credentialIdentifiersSupported = true, signedMetadata = null, display = display.map { it.toTransferObject() }.takeIf { it.isNotEmpty() }, diff --git a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt index e822af0..70b9735 100644 --- a/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt +++ b/src/main/kotlin/eu/europa/ec/eudi/pidissuer/port/input/IssueCredential.kt @@ -317,7 +317,10 @@ class IssueCredential( ): IssueCredentialResponse = coroutineScope { either { log.info("Handling issuance request for ${credentialRequestTO.format}..") - val unresolvedRequest = credentialRequestTO.toDomain(credentialIssuerMetadata.credentialResponseEncryption) + val unresolvedRequest = credentialRequestTO.toDomain( + credentialIssuerMetadata.credentialResponseEncryption, + credentialIssuerMetadata.batchCredentialIssuance, + ) val (request, credentialIdentifier) = when (unresolvedRequest) { is UnresolvedCredentialRequest.ByFormat -> @@ -428,13 +431,26 @@ private sealed interface UnresolvedCredentialRequest { ) : UnresolvedCredentialRequest } +private val BatchCredentialIssuance.maxProofsSupported: Int + get() = when (this) { + BatchCredentialIssuance.NotSupported -> 1 + is BatchCredentialIssuance.Supported -> batchSize + } + /** * Tries to convert a [CredentialRequestTO] to a [CredentialRequest]. */ context(Raise) private fun CredentialRequestTO.toDomain( - supported: CredentialResponseEncryption, + supportedEncryption: CredentialResponseEncryption, + supportedBatchIssuance: BatchCredentialIssuance, ): UnresolvedCredentialRequest { + if (supportedBatchIssuance is BatchCredentialIssuance.NotSupported) { + ensure(proofs == null) { + InvalidProof("Credential Endpoint does not support Batch Issuance") + } + } + val proofs = when { proof != null && proofs == null -> nonEmptyListOf(proof.toDomain()) @@ -453,10 +469,13 @@ private fun CredentialRequestTO.toDomain( proof != null && proofs != null -> raise(InvalidProof("Only one of `proof` or `proofs` is allowed")) else -> raise(MissingProof) } + ensure(proofs.size <= supportedBatchIssuance.maxProofsSupported) { + InvalidProof("You can provide at most '${supportedBatchIssuance.maxProofsSupported}' proofs") + } val credentialResponseEncryption = credentialResponseEncryption?.toDomain() ?: RequestedResponseEncryption.NotRequired - credentialResponseEncryption.ensureIsSupported(supported) + credentialResponseEncryption.ensureIsSupported(supportedEncryption) fun credentialRequestByFormat(format: FormatTO): UnresolvedCredentialRequest.ByFormat = when (format) { @@ -609,6 +628,7 @@ fun CredentialResponse.toTO(nonce: CNonce): IssueCredentialResponse.PlainTO = wh nonceExpiresIn = nonce.expiresIn.toSeconds(), notificationId = notificationId?.value, ) + else -> IssueCredentialResponse.PlainTO.multiple( credentials = JsonArray(credentials), nonce = nonce.nonce, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 8a077b4..cff2122 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -38,6 +38,8 @@ issuer.signing-key=GenerateRandom issuer.dpop.proof-max-age=PT1M issuer.dpop.cache-purge-interval=PT10M issuer.dpop.realm=pid-issuer +issuer.credentialEndpoint.batchIssuance.enabled=true +issuer.credentialEndpoint.batchIssuance.batchSize=10 # # Resource Server configuration