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

Align SD-JWT-VC PID with PID rulebook and include signing key information in SD-JWT-VC header claims #249

Merged
Merged
Show file tree
Hide file tree
Changes from 13 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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,10 +179,14 @@ Variable: `ISSUER_PID_SD_JWT_VC_NOTIFICATIONS_ENABLED`
Description: Whether to enabled Notifications Endpoint support for PIDs issued in *SD JWT VC*.
Default value: `true`

Variable: `ISSUER_PID_ISSUING_COUNTRY`
Variable: `ISSUER_PID_ISSUINGCOUNTRY`
Description: Code of the Country issuing the PID
Default value: `GR`

Variable: `ISSUER_PID_ISSUINGJURISDICTION`
Description: Country subdivision code of the jurisdiction issuing the PID
Default value: `GR-I`

Variable: `ISSUER_MDL_ENABLED`
Description: Whether to enable support for issuing mDL.
Default value: `true`
Expand Down
1 change: 1 addition & 0 deletions docker-compose/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ services:
- ISSUER_PID_SD_JWT_VC_DEFERRED=true
- ISSUER_PID_SD_JWT_VC_NOTIFICATIONS_ENABLED=true
- ISSUER_PID_ISSUINGCOUNTRY=GR
- ISSUER_PID_ISSUINGJURISDICTION=GR-I
- ISSUER_MDL_ENABLED=true
- ISSUER_MDL_MSO_MDOC_ENCODER_DURATION=P5D
- ISSUER_MDL_NOTIFICATIONS_ENABLED=true
Expand Down
274 changes: 273 additions & 1 deletion docker-compose/keycloak/realms/pid-issuer-realm-realm.json

Large diffs are not rendered by default.

23 changes: 22 additions & 1 deletion docker-compose/keycloak/realms/pid-issuer-realm-users-0.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
"true"
],
"street": [
"101 Trauner"
"Trauner"
],
"address_house_number": [
"101"
],
"locality": [
"Gemeinde Biberbach"
Expand All @@ -35,6 +38,24 @@
],
"country": [
"AT"
],
"birth_country": [
"AT"
],
"birth_city": [
"Gemeinde Biberbach"
],
"birth_place": [
"101 Trauner"
],
"nationality": [
"AT"
],
"birth_family_name": [
"Neal"
],
"birth_given_name": [
"Tyler"
]
},
"credentials": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,11 @@ fun beans(clock: Clock) = beans {
.build()

GetPidDataFromAuthServer(
env.getRequiredProperty("issuer.pid.issuingCountry").let(::IsoCountry),
clock,
keycloak,
keycloakProperties.userRealm,
issuerCountry = env.getRequiredProperty("issuer.pid.issuingCountry").let(::IsoCountry),
issuingJurisdiction = env.getProperty("issuer.pid.issuingJurisdiction"),
clock = clock,
keycloak = keycloak,
userRealm = keycloakProperties.userRealm,
)
}
bean<EncodePidInCbor>(isLazyInit = true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,41 +39,41 @@ val OidcSub: AttributeDetails by lazy {
val OidcFamilyName: AttributeDetails by lazy {
AttributeDetails(
name = "family_name",
display = mapOf(Locale.ENGLISH to "Current Family Name"),
display = mapOf(Locale.ENGLISH to "Current last name(s) or surname(s) of the PID User."),
)
}

val OidcGivenName: AttributeDetails by lazy {
AttributeDetails(
name = "given_name",
display = mapOf(Locale.ENGLISH to "Current First Names"),
display = mapOf(Locale.ENGLISH to "Current first name(s), including middle name(s), of the PID User."),
)
}

val OidcBirthDate: AttributeDetails by lazy {
AttributeDetails(
name = "birthdate",
display = mapOf(Locale.ENGLISH to "Date of Birth"),
display = mapOf(Locale.ENGLISH to "Day, month, and year on which the PID User was born."),
)
}

val OidcGender: AttributeDetails by lazy {
AttributeDetails(
name = "gender",
mandatory = false,
display = mapOf(Locale.ENGLISH to "PID User’s gender, using a value as defined in ISO/IEC 5218."),
display = mapOf(Locale.ENGLISH to "PID User’s gender, using a value as defined in OpenID Connect Core 1.0."),
)
}

// https://openid.net/specs/openid-connect-core-1_0.html#AddressClaim
@Serializable
data class OidcAddressClaim(
@SerialName("street_address") val streetAddress: String? = null,
val locality: String? = null,
val region: String? = null,
@SerialName("locality") val locality: String? = null,
@SerialName("region") val region: String? = null,
@SerialName("postal_code") val postalCode: String? = null,
val country: String? = null,
val formatted: String? = null,
@SerialName("country") val country: String? = null,
@SerialName("formatted") val formatted: String? = null,
@SerialName("house_number") val houseNumber: String? = null,
) {

Expand All @@ -84,7 +84,8 @@ data class OidcAddressClaim(
name = NAME,
mandatory = false,
display = mapOf(
Locale.ENGLISH to "Resident street_address, country, region, locality and postal_code",
Locale.ENGLISH to "The full address of the place where the PID User currently resides and/or " +
"can be contacted (street name, house number, city etc.).",
),
)
}
Expand All @@ -98,7 +99,7 @@ val OidcAssuranceNationalities: AttributeDetails by lazy {
AttributeDetails(
name = "nationalities",
mandatory = false,
display = mapOf(Locale.ENGLISH to "Array of nationalities"),
display = mapOf(Locale.ENGLISH to "Alpha-2 country code as specified in ISO 3166-1, representing the nationality of the PID User."),
)
}

Expand Down Expand Up @@ -129,7 +130,7 @@ data class OidcAssurancePlaceOfBirth(
get() = AttributeDetails(
name = NAME,
mandatory = false,
display = mapOf(Locale.ENGLISH to "The country, region, and locality"),
display = mapOf(Locale.ENGLISH to "The country, state, and city where the PID User was born."),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import org.slf4j.LoggerFactory
import java.time.Clock
import java.time.Instant
import java.time.ZonedDateTime
import eu.europa.ec.eudi.pidissuer.adapter.out.oauth.OidcGender as OidcGenderAttribute

private val log = LoggerFactory.getLogger(EncodePidInSdJwtVc::class.java)

Expand Down Expand Up @@ -127,37 +128,26 @@ private fun selectivelyDisclosed(
//
// Selectively Disclosed claims
//
// https://openid.net/specs/openid-connect-4-identity-assurance-1_0.html#section-4
sd(Attributes.IssuanceDate.name, pidMetaData.issuanceDate.toString()) // TODO Check issuance date
sd(OidcGivenName.name, pid.givenName.value)
// TODO Check also_known_as
sd(OidcFamilyName.name, pid.familyName.value)
sd(OidcGivenName.name, pid.givenName.value)
sd(OidcBirthDate.name, pid.birthDate.toString())
structured(Attributes.AgeEqualOrOver.name) {
pid.ageOver18?.let { sd(Attributes.AgeOver18.name, it) }
}

// TODO
// Here we need a mapping in OIDC gender can be male, female on null
// In PID the use iso
pid.gender?.let { sd(OidcGender.name, it.value.toInt()) }
pid.nationality?.let {
val nationalities = buildJsonArray { add(it.value) }
sd(OidcAssuranceNationalities.name, nationalities)
}
pid.ageInYears?.let { sd(Attributes.AgeInYears.name, it.toInt()) }
pid.ageBirthYear?.let { sd(Attributes.BirthDateYear.name, it.value.toString()) }
pid.ageBirthYear?.let { sd(Attributes.AgeBirthYear.name, it.value.toString()) }
pid.familyNameBirth?.let { sd(OidcAssuranceBirthFamilyName.name, it.value) }
pid.givenNameBirth?.let { sd(OidcAssuranceBirthGivenName.name, it.value) }

pid.oidcAssurancePlaceOfBirth()?.let { placeOfBirth ->
recursive(OidcAssurancePlaceOfBirth.NAME) {
structured(OidcAssurancePlaceOfBirth.NAME) {
placeOfBirth.locality?.let { sd("locality", it) }
placeOfBirth.region?.let { sd("region", it) }
placeOfBirth.country?.let { sd("country", it) }
}
}
pid.oidcAddressClaim()?.let { address ->
recursive(OidcAddressClaim.NAME) {
structured(OidcAddressClaim.NAME) {
address.formatted?.let { sd("formatted", it) }
address.country?.let { sd("country", it) }
address.region?.let { sd("region", it) }
Expand All @@ -167,30 +157,45 @@ private fun selectivelyDisclosed(
address.houseNumber?.let { sd("house_number", it) }
}
}
pid.gender?.let { sd(OidcGenderAttribute.name, it.toOidGender().value) }
pid.nationality?.let {
val nationalities = buildJsonArray { add(it.value) }
sd(OidcAssuranceNationalities.name, nationalities)
}
sd(IssuingAuthorityAttribute.name, pidMetaData.issuingAuthority.valueAsString())
pidMetaData.documentNumber?.let { sd(DocumentNumberAttribute.name, it.value) }
pidMetaData.administrativeNumber?.let { sd(AdministrativeNumberAttribute.name, it.value) }
sd(IssuingCountryAttribute.name, pidMetaData.issuingCountry.value)
pidMetaData.issuingJurisdiction?.let { sd(IssuingJurisdictionAttribute.name, it) }
}
}

private fun Pid.oidcAssurancePlaceOfBirth(): OidcAssurancePlaceOfBirth? =
if (birthCountry != null || birthState != null || birthCity != null) {
if (birthPlace != null || birthCountry != null || birthState != null || birthCity != null) {
// TODO
// birth_place and birth_city are both mapped to locality
// https://github.com/eu-digital-identity-wallet/eudi-doc-architecture-and-reference-framework/pull/160#discussion_r1853638874
OidcAssurancePlaceOfBirth(
locality = birthPlace ?: birthCity?.value,
country = birthCountry?.value,
region = residentState?.value,
locality = residentCity?.value,
region = birthState?.value,
)
} else null

private fun Pid.oidcAddressClaim(): OidcAddressClaim? =
if (
residentCountry != null || residentState != null ||
residentAddress != null || residentCountry != null || residentState != null ||
residentCity != null || residentPostalCode != null ||
residentStreet != null
residentStreet != null || residentHouseNumber != null
) {
OidcAddressClaim(
formatted = residentAddress,
country = residentCountry?.value,
region = residentState?.value,
locality = residentCity?.value,
postalCode = residentPostalCode?.value,
streetAddress = residentStreet?.value,
houseNumber = residentHouseNumber,
)
} else null

Expand All @@ -213,3 +218,15 @@ private object Printer {
return str
}
}

/**
* Converts an [IsoGender] to an [OidcGender].
*/
private fun IsoGender.toOidGender(): OidcGender =
dzarras marked this conversation as resolved.
Show resolved Hide resolved
when (value) {
0u -> OidcGender("not known")
1u -> OidcGender.Male
2u -> OidcGender.Female
9u -> OidcGender("not applicable")
else -> OidcGender(value.toString())
}
Loading