Skip to content

Commit

Permalink
Final bits for batch credential issuance.
Browse files Browse the repository at this point in the history
  • Loading branch information
dzarras committed Sep 26, 2024
1 parent f832695 commit de4c82d
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 10 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,15 @@ fun beans(clock: Clock) = beans {
add(mdlIssuer)
}
},
batchCredentialIssuance = run {
val enabled = env.getProperty<Boolean>("issuer.credentialEndpoint.batchIssuance.enabled") ?: true
if (enabled) {
val batchSize = env.getProperty<Int>("issuer.credentialEndpoint.batchIssuance.batchSize") ?: 10
BatchCredentialIssuance.Supported(batchSize)
} else {
BatchCredentialIssuance.NotSupported
}
},
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -109,7 +107,7 @@ data class CredentialIssuerMetaData(
val id: CredentialIssuerId,
val authorizationServers: List<HttpsUrl>,
val credentialEndPoint: HttpsUrl,
val batchCredentialEndpoint: HttpsUrl? = null,
val batchCredentialIssuance: BatchCredentialIssuance,
val deferredCredentialEndpoint: HttpsUrl? = null,
val notificationEndpoint: HttpsUrl? = null,
val credentialResponseEncryption: CredentialResponseEncryption,
Expand All @@ -119,3 +117,23 @@ data class CredentialIssuerMetaData(
val credentialConfigurationsSupported: List<CredentialConfiguration>
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" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,14 @@ data class CredentialIssuerMetaDataTO(
val authorizationServers: List<String>? = 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")
Expand All @@ -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
Expand All @@ -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() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ->
Expand Down Expand Up @@ -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<IssueCredentialError>)
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())
Expand All @@ -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) {
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit de4c82d

Please sign in to comment.