Skip to content

Commit

Permalink
Align SD-JWT-VC PID with PID rulebook and include signing key informa…
Browse files Browse the repository at this point in the history
…tion in SD-JWT-VC header claims (#249)

---------

Co-authored-by: babisRoutis <haralampos.routis@netcompany.com>
  • Loading branch information
dzarras and babisRoutis authored Nov 25, 2024
1 parent fa54cc5 commit abd357b
Show file tree
Hide file tree
Showing 13 changed files with 498 additions and 102 deletions.
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
308 changes: 307 additions & 1 deletion docker-compose/keycloak/realms/pid-issuer-realm-realm.json

Large diffs are not rendered by default.

26 changes: 25 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 @@ -15,14 +15,20 @@
"gender": [
"1"
],
"gender_as_string": [
"male"
],
"birthdate": [
"1955-04-12"
],
"age_over_18": [
"true"
],
"street": [
"101 Trauner"
"Trauner"
],
"address_house_number": [
"101"
],
"locality": [
"Gemeinde Biberbach"
Expand All @@ -35,6 +41,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 @@ -66,7 +66,10 @@ class EncodePidInSdJwtVc(
val sdJwtFactory = SdJwtFactory(hashAlgorithm = hashAlgorithm, fallbackMinimumDigests = null)
val signer = ECDSASigner(issuerSigningKey.key)
SdJwtIssuer.nimbus(sdJwtFactory, signer, issuerSigningKey.signingAlgorithm) {
// TODO: This will change to dc+sd-jwt in a future release
type(JOSEObjectType("vc+sd-jwt"))
keyID(issuerSigningKey.key.keyID)
x509CertChain(issuerSigningKey.key.x509CertChain)
}
}

Expand Down Expand Up @@ -127,37 +130,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 +159,45 @@ private fun selectivelyDisclosed(
address.houseNumber?.let { sd("house_number", it) }
}
}
pid.genderAsString?.let { sd(OidcGender.name, it) }
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 Down
Loading

0 comments on commit abd357b

Please sign in to comment.