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

Add support for mDL issuance #79

Merged
merged 3 commits into from
Jan 18, 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
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,15 @@ the [EUDI Wallet Reference Implementation project description](https://github.co
An implementation of a credential issuing service, according to
[OpenId4VCI - draft12](https://openid.github.io/OpenID4VCI/openid-4-verifiable-credential-issuance-wg-draft.html)

The service provides generic support for `mso_mdoc` and `SD-JWT-VC` formats using PID as an example
The service provides generic support for `mso_mdoc` and `SD-JWT-VC` formats using PID and mDL as an example
and requires the use of a suitable OAUTH2 server.

| Credential/Attestation | Format |
|------------------------|-----------|
| PID | mso_mdoc |
| PID | SD-JWT-VC |
| mDL | mso_mdoc |

### OpenId4VCI coverage

| Feature | Coverage |
Expand All @@ -45,20 +51,21 @@ A Keycloak instance accessible via https://localhost/idp/ with the Realm *pid-is

The Realm *pid-issuer-realm*:

- has user self-registration active with a custom registration page accessible via https://localhost/idp/realms/pid-issuer-realm/account/#/
- has user self-registration active with a custom registration page accessible
via https://localhost/idp/realms/pid-issuer-realm/account/#/
- defines *eu.europa.ec.eudiw.pid_vc_sd_jwt* scope for requesting PID issuance in SD JWT VC format
- defines *eu.europa.ec.eudiw.pid_mso_mdoc* scope for requesting PID issuance in MSO MDOC format
- defines *wallet-dev* and *pid-issuer-srv* clients
- contains sample user with credentials: tneal / password

Administration console is accessible via https://localhost/idp/admin/ using the credentials admin / password

### PID Issuer
### PID mDL Issuer

A PID Issuer instance accessible via https://localhost/pid-issuer/
A PID mDL Issuer instance accessible via https://localhost/pid-issuer/

It uses the configured Keycloak instance as an Authorization Server, and PID issuance both *SD JWT VC* and *MSO MDOC*
formats is enabled. Additionally *deferred issuance* is enabled for *SD JWT VC* format.
It uses the configured Keycloak instance as an Authorization Server, and supports issuing of PID and mDL.
Additionally, *deferred issuance* is enabled for PID in *SD JWT VC* format.

The issuing country is set to GR (Greece).

Expand Down Expand Up @@ -147,7 +154,6 @@ curl http://localhost:8080/.well-known/openid-credential-issuer | jq .

### Credential Endpoint


### Credentials Offer

Generate sample offer
Expand Down
80 changes: 77 additions & 3 deletions docker-compose/keycloak/realms/pid-issuer-realm-realm.json
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,12 @@
"roles": [
"eid-holder-natural-person"
]
},
{
"clientScope": "org.iso.18013.5.1.mDL",
"roles": [
"eid-holder-natural-person"
]
}
],
"clientScopeMappings": {
Expand Down Expand Up @@ -709,7 +715,8 @@
],
"optionalClientScopes": [
"eu.europa.ec.eudiw.pid_vc_sd_jwt",
"eu.europa.ec.eudiw.pid_mso_mdoc"
"eu.europa.ec.eudiw.pid_mso_mdoc",
"org.iso.18013.5.1.mDL"
]
},
{
Expand Down Expand Up @@ -1012,7 +1019,8 @@
"optionalClientScopes": [
"roles",
"eu.europa.ec.eudiw.pid_vc_sd_jwt",
"eu.europa.ec.eudiw.pid_mso_mdoc"
"eu.europa.ec.eudiw.pid_mso_mdoc",
"org.iso.18013.5.1.mDL"
]
}
],
Expand Down Expand Up @@ -1364,6 +1372,71 @@
}
]
},
{
"id": "261a329e-327b-43fa-849b-5c3c8748c663",
"name": "org.iso.18013.5.1.mDL",
"description": "",
"protocol": "openid-connect",
"attributes": {
"include.in.token.scope": "true",
"display.on.consent.screen": "true",
"gui.order": "",
"consent.screen.text": "Do you consent to issue mDL?"
},
"protocolMappers": [
{
"id": "d06095b4-af59-40e1-ad1a-017c5c1f8473",
"name": "given name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"aggregate.attrs": "false",
"userinfo.token.claim": "true",
"multivalued": "false",
"user.attribute": "firstName",
"id.token.claim": "false",
"access.token.claim": "false",
"claim.name": "given_name",
"jsonType.label": "String"
}
},
{
"id": "7b14d41e-74ec-4cf8-bc07-9afb932a797e",
"name": "family name",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"aggregate.attrs": "false",
"userinfo.token.claim": "true",
"multivalued": "false",
"user.attribute": "lastName",
"id.token.claim": "false",
"access.token.claim": "false",
"claim.name": "family_name",
"jsonType.label": "String"
}
},
{
"id": "1ab7730f-9a35-4587-86de-1fc2db219989",
"name": "email",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"consentRequired": false,
"config": {
"aggregate.attrs": "false",
"userinfo.token.claim": "true",
"multivalued": "false",
"user.attribute": "email",
"id.token.claim": "false",
"access.token.claim": "false",
"claim.name": "email",
"jsonType.label": "String"
}
}
]
},
{
"id": "00bf2e53-5336-47ef-819f-3f1823a2cc81",
"name": "roles",
Expand Down Expand Up @@ -1419,7 +1492,8 @@
],
"defaultOptionalClientScopes": [
"eu.europa.ec.eudiw.pid_mso_mdoc",
"eu.europa.ec.eudiw.pid_vc_sd_jwt"
"eu.europa.ec.eudiw.pid_vc_sd_jwt",
"org.iso.18013.5.1.mDL"
],
"browserSecurityHeaders": {
"contentSecurityPolicyReportOnly": "",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import eu.europa.ec.eudi.pidissuer.adapter.input.web.MetaDataApi
import eu.europa.ec.eudi.pidissuer.adapter.input.web.WalletApi
import eu.europa.ec.eudi.pidissuer.adapter.out.jose.DefaultExtractJwkFromCredentialKey
import eu.europa.ec.eudi.pidissuer.adapter.out.jose.EncryptCredentialResponseWithNimbus
import eu.europa.ec.eudi.pidissuer.adapter.out.mdl.EncodeMobileDrivingLicenceInCborWithMicroservice
import eu.europa.ec.eudi.pidissuer.adapter.out.mdl.GetMobileDrivingLicenceDataMock
import eu.europa.ec.eudi.pidissuer.adapter.out.mdl.IssueMobileDrivingLicence
import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryCNonceRepository
import eu.europa.ec.eudi.pidissuer.adapter.out.persistence.InMemoryDeferredCredentialRepository
import eu.europa.ec.eudi.pidissuer.adapter.out.pid.*
Expand Down Expand Up @@ -77,7 +80,7 @@ private val log = LoggerFactory.getLogger(PidIssuerApplication::class.java)
/**
* [WebClient] instances for usage within the application.
*/
private object WebClients {
internal object WebClients {

/**
* A [WebClient] with [Json] serialization enabled.
Expand Down Expand Up @@ -129,6 +132,18 @@ fun beans(clock: Clock) = beans {
ref(),
)
}
bean {
EncodePidInCborWithMicroService(env.readRequiredUrl("issuer.pid.mso_mdoc.encoderUrl"), ref())
}
bean {
GetMobileDrivingLicenceDataMock()
}
bean {
EncodeMobileDrivingLicenceInCborWithMicroservice(
ref(),
env.readRequiredUrl("issuer.mdl.mso_mdoc.encoderUrl"),
)
}
//
// Encryption of credential response
//
Expand Down Expand Up @@ -161,10 +176,6 @@ fun beans(clock: Clock) = beans {
bean {
val issuerPublicUrl = env.readRequiredUrl("issuer.publicUrl", removeTrailingSlash = true)

bean {
EncodePidInCborWithMicroService(env.readRequiredUrl("issuer.pid.mso_mdoc.encoderUrl"), ref())
}

CredentialIssuerMetaData(
id = issuerPublicUrl,
credentialEndPoint = issuerPublicUrl.appendPath(WalletApi.CREDENTIAL_ENDPOINT),
Expand Down Expand Up @@ -216,6 +227,12 @@ fun beans(clock: Clock) = beans {
else issueSdJwtVcPid,
)
}

val enableMobileDrivingLicence = env.getProperty("issuer.mdl.enabled", true)
if (enableMobileDrivingLicence) {
val mdlIssuer = IssueMobileDrivingLicence(issuerPublicUrl, ref(), ref())
add(mdlIssuer)
}
},
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2023 European Commission
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.europa.ec.eudi.pidissuer.adapter.out.jose

import com.nimbusds.jose.jwk.ECKey
import org.bouncycastle.util.io.pem.PemObject
import org.bouncycastle.util.io.pem.PemWriter
import java.io.StringWriter
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

internal fun ECKey.toPem(): String =
StringWriter().use { stringWriter ->
PemWriter(stringWriter).use { pemWriter ->
pemWriter.writeObject(PemObject("PUBLIC KEY", this.toECPublicKey().encoded))
}
stringWriter.toString()
}

@OptIn(ExperimentalEncodingApi::class)
internal fun ECKey.toBase64UrlSafeEncodedPem(): String = Base64.UrlSafe.encode(this.toPem().toByteArray())
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 European Commission
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.europa.ec.eudi.pidissuer.adapter.out.mdl

import arrow.core.raise.Raise
import com.nimbusds.jose.jwk.ECKey
import eu.europa.ec.eudi.pidissuer.port.input.IssueCredentialError

/**
* Encodes a Mobile Driving Licence in CBOR format.
*/
fun interface EncodeMobileDrivingLicenceInCbor {

context(Raise<IssueCredentialError.Unexpected>)
suspend operator fun invoke(licence: MobileDrivingLicence, holderKey: ECKey): String
}
Loading