Skip to content
Open
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
6 changes: 3 additions & 3 deletions .github/config/test-strategy-matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,18 @@
{
"os": "ubuntu-latest",
"name": "jvmRunner",
"testCommand": "./gradlew jvmTest && (cd mobile-driving-licence-credential && ./gradlew -DregressionTest=true jvmTest )"
"testCommand": "./gradlew jvmTest"
},
{
"os": "ubuntu-latest",
"name": "androidRunner",
"enableKvm": true,
"testCommand": "./gradlew pixelAVDAndroidDeviceTest "
"testCommand": "./gradlew pixelAVDAndroidDeviceTest"
},
{
"os": "macos-latest",
"name": "iosRunner",
"testCommand": "./gradlew iosArm64MainKlibrary iosSimulatorArm64MainKlibrary iosSimulatorArm64Test && (cd mobile-driving-licence-credential && ./gradlew -DregressionTest=true iosSimulatorArm64Test )"
"testCommand": "./gradlew iosArm64MainKlibrary iosSimulatorArm64MainKlibrary iosSimulatorArm64Test"
}
]
}
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
[submodule "conventions-vclib/gradle-conventions-plugin"]
path = conventions-vclib/gradle-conventions-plugin
url = https://github.com/a-sit-plus/gradle-conventions-plugin.git
[submodule "mobile-driving-licence-credential"]
path = mobile-driving-licence-credential
url = https://github.com/a-sit-plus/mobile-driving-licence-credential.git
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Release 6.0.0 (unreleased):
- In `CredentialScheme` deprecate `claimNames` (list of strings), to be replaced with `claimDescriptions` (set of typed descriptions)
- In `StoreEntry` deprecate property `scheme` and add suspending function `resolveScheme()` to replace it
- Add `UnknownCredentialScheme` so that the `scheme` property of several classes is never null
- Import data classes and data element strings from credentials into this library for [EU PID](https://github.com/a-sit-plus/eu-pid-credential), [EU PID in SD-JWT](https://github.com/a-sit-plus/eu-pid-credential-sdjwt/) and [Mobile Driving Licence](https://github.com/a-sit-plus/mobile-driving-licence-credential/)
- New modules:
- `etsi-data-classes` implements list of trusted entities from [ETSI TS 119 602](https://www.etsi.org/deliver/etsi_ts/119600_119699/119602/01.01.01_60/ts_119602v010101p.pdf)
- `sd-jwt-type-metadata` implements SD-JWT VC Type Metadata from [draft-ietf-oauth-sd-jwt-vc-16](https://datatracker.ietf.org/doc/draft-ietf-oauth-sd-jwt-vc/):
Expand Down
3 changes: 0 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ uuid = "0.8.1"
jsonpath = "3.0.0"
jvmJson = "20230618"
jvmCbor = "1.21"
eupid = "3.5.0-SNAPSHOT"
eupidsdjwt = "1.4.0"
mdl = "1.4.0"
obor = "2.1.3"
#testballoonAddons = "0.15.0"

1 change: 0 additions & 1 deletion mobile-driving-licence-credential
Submodule mobile-driving-licence-credential deleted from e7d8ac
3 changes: 0 additions & 3 deletions sd-jwt-type-metadata/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import at.asitplus.gradle.VcLibVersions
import at.asitplus.gradle.envExtra
import at.asitplus.gradle.exportXCFramework
import at.asitplus.gradle.ktor
import at.asitplus.gradle.napier
import at.asitplus.gradle.setupDokka
import at.asitplus.gradle.vckAndroid
Expand Down Expand Up @@ -30,8 +29,6 @@ kotlin {
commonMain {
dependencies {
implementation(project.napier())
implementation(project.ktor("http"))
implementation(ktor("client-core"))
api(project(":rfc3986-uri-syntax"))
api("at.asitplus.signum:supreme:${VcLibVersions.supreme}")
api("at.asitplus.signum:indispensable:${VcLibVersions.signum}")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package at.asitplus.wallet.sdjwt

import at.asitplus.wallet.sdjwt.SdJwtTypeMetadataDefinition.SerialNames
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class SdJwtTypeMetadata(
@SerialName(SdJwtTypeMetadataDefinition.SerialNames.VCT)
@SerialName(SerialNames.VCT)
val vct: SdJwtVcType,
@SerialName(SdJwtTypeMetadataDefinition.SerialNames.NAME)
@SerialName(SerialNames.NAME)
val name: String? = null,
@SerialName(SdJwtTypeMetadataDefinition.SerialNames.DESCRIPTION)
@SerialName(SerialNames.DESCRIPTION)
val description: String? = null,
@SerialName(SdJwtTypeMetadataDefinition.SerialNames.DISPLAY)
@SerialName(SerialNames.DISPLAY)
val display: SdJwtTypeMetadataTypeDisplayInformationList? = null,
@SerialName(SdJwtTypeMetadataDefinition.SerialNames.CLAIMS)
@SerialName(SerialNames.CLAIMS)
val claims: SdJwtTypeMetadataClaimInformationList? = null,
)
@SerialName(SerialNames.VCK)
val vckExtensions: SdJwtTypeMetadataVckExtensions? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ data class SdJwtTypeMetadataDefinition(
val display: SdJwtTypeMetadataTypeDisplayInformationList? = null,
@SerialName(SerialNames.CLAIMS)
val claims: SdJwtTypeMetadataClaimInformationList? = null,
@SerialName(SerialNames.VCK)
val vckExtensions: SdJwtTypeMetadataVckExtensions? = null,
) {
object SerialNames {
const val VCT = "vct"
Expand All @@ -35,6 +37,7 @@ data class SdJwtTypeMetadataDefinition(
const val EXTENDS_INTEGRITY = "extends#integrity"
const val DISPLAY = "display"
const val CLAIMS = "claims"
const val VCK = "vck"
}

fun toSdJwtTypeMetadata(): SdJwtTypeMetadata {
Expand All @@ -47,6 +50,7 @@ data class SdJwtTypeMetadataDefinition(
description = description,
display = display,
claims = claims,
vckExtensions = vckExtensions,
)
}

Expand Down Expand Up @@ -80,7 +84,8 @@ data class SdJwtTypeMetadataDefinition(
}
childClaimInfo.extendFrom(baseClaimInfo)
}.values.filterNotNull()
}?.let(::SdJwtTypeMetadataClaimInformationList) ?: base.claims
}?.let(::SdJwtTypeMetadataClaimInformationList) ?: base.claims,
vckExtensions = vckExtensions ?: base.vckExtensions,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package at.asitplus.wallet.sdjwt

import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

@Serializable
data class SdJwtTypeMetadataVckExtensions(
@SerialName(SerialNames.FORMAT)
val format: CredentialFormatEnum,
@SerialName(SerialNames.ISO_DOCTYPE)
val isoDocType: String? = null,
@SerialName(SerialNames.ISO_NAMESPACE)
val isoNamespace: String? = null,
@SerialName(SerialNames.VC_TYPE)
val vcType: String? = null,
) {

object SerialNames {
const val ISO_DOCTYPE = "isoDocType"
const val ISO_NAMESPACE = "isoNamespace"
const val VC_TYPE = "vcType"
const val FORMAT = "format"
}

}

@Serializable(with = CredentialFormatEnum.Companion.Serializer::class)
enum class CredentialFormatEnum(val text: String) {
JWT_VC("jwt_vc_json"),
DC_SD_JWT("dc+sd-jwt"),
MSO_MDOC("mso_mdoc");


companion object {
fun parse(text: String) = CredentialFormatEnum.entries.firstOrNull { it.text == text }

object Serializer : KSerializer<CredentialFormatEnum> {

override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("CredentialFormatEnumSerializer", PrimitiveKind.STRING)

override fun serialize(encoder: Encoder, value: CredentialFormatEnum) {
encoder.encodeString(value.text)
}

override fun deserialize(decoder: Decoder): CredentialFormatEnum {
val text = decoder.decodeString()
return CredentialFormatEnum.parse(text) ?: throw IllegalArgumentException(text)
}
}
}
}
3 changes: 0 additions & 3 deletions vck-openid-ktor/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,6 @@ kotlin {

commonTest {
dependencies {
implementation("at.asitplus.wallet:eupidcredential:${VcLibVersions.eupid}")
implementation("at.asitplus.wallet:eupidcredential-sdjwt:${VcLibVersions.eupidsdjwt}")
implementation("at.asitplus.wallet:mobiledrivinglicence:${VcLibVersions.mdl}")
implementation(ktor("client-mock"))
implementation(kotest("assertions-core"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package at.asitplus.wallet.sdjwt
package at.asitplus.wallet.lib.ktor.openid

import at.asitplus.rfc3986uri.Rfc3986UniformResourceIdentifier
import at.asitplus.rfc3986uri.Rfc3986UriSchemeName
import at.asitplus.wallet.sdjwt.SdJwtTypeMetadataDefinition
import at.asitplus.wallet.sdjwt.SdJwtTypeMetadataDocument
import at.asitplus.wallet.sdjwt.SdJwtTypeMetadataDocumentIntegrityChecker
import at.asitplus.wallet.sdjwt.SdJwtTypeMetadataDocumentRetriever
import at.asitplus.wallet.sdjwt.SdJwtVcType
import at.asitplus.wallet.sdjwt.W3cSubresourceIntegrityMetadata
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
Expand All @@ -13,11 +19,16 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Instant

// unused because there are currently no officially available type metadata documents
@Suppress("unused")
class KtorSdJwtTypeMetadataDocumentRetriever(
val httpClient: HttpClient,
val clock: Clock,
/**
* Resolves the URL a metadata document is hosted at for a given `vct` (used both as document identity and for
* walking `extends`). Required because a `vct` is not necessarily a URL (e.g. `urn:eudi:pid:1`); the owning
* [at.asitplus.wallet.lib.data.CredentialMetadataRegistry] keeps the actual `vct -> URL` mapping and supplies this
* lookup. A `vct` for which this returns `null` cannot be fetched, and [retrieve] returns `null` for it.
*/
val locateUrl: (SdJwtVcType) -> String?,
val json: Json = Json.Default,
val integrityChecker: SdJwtTypeMetadataDocumentIntegrityChecker = SdJwtTypeMetadataDocumentIntegrityChecker.DEFAULT,
) : SdJwtTypeMetadataDocumentRetriever {
Expand All @@ -28,8 +39,10 @@ class KtorSdJwtTypeMetadataDocumentRetriever(
sdJwtVcType: SdJwtVcType,
integrityMetadata: W3cSubresourceIntegrityMetadata?,
): SdJwtTypeMetadataDocument? {
val url = locateUrl(sdJwtVcType) ?: return null

val uri = runCatching {
Rfc3986UniformResourceIdentifier.Companion(sdJwtVcType.string)
Rfc3986UniformResourceIdentifier.Companion(url)
Comment thread
JesusMcCloud marked this conversation as resolved.
}.getOrNull() ?: return null

if (uri.schemeName !in Rfc3986UriSchemeName.Common.run { listOf(HTTPS, HTTP) }) {
Expand All @@ -52,7 +65,7 @@ class KtorSdJwtTypeMetadataDocumentRetriever(
}
}

val response = httpClient.get(sdJwtVcType.string)
val response = httpClient.get(url)
if (response.status == HttpStatusCode.OK) {
val rawBytes = response.body<ByteArray>()
val definition = json.decodeFromString(SdJwtTypeMetadataDefinition.serializer(), rawBytes.decodeToString())
Expand Down Expand Up @@ -141,4 +154,4 @@ class KtorSdJwtTypeMetadataDocumentRetriever(
dynamicCache[sdJwtVcType] = validUntil to document
return true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package at.asitplus.wallet.lib.ktor.openid

import at.asitplus.catching
import at.asitplus.wallet.lib.data.ConstantIndex.CredentialRepresentation
import at.asitplus.wallet.lib.data.ConstantIndex.CredentialRepresentation.SD_JWT
import at.asitplus.wallet.lib.data.CredentialMetadataLookup
import at.asitplus.wallet.lib.data.CredentialMetadataRegistry
import at.asitplus.wallet.lib.data.ResolvedCredentialMetadata
import at.asitplus.wallet.sdjwt.DelegatingSdJwtTypeMetadataDocumentResolver
import at.asitplus.wallet.sdjwt.SdJwtTypeMetadataDocumentIntegrityChecker
import at.asitplus.wallet.sdjwt.SdJwtVcType
import at.asitplus.wallet.sdjwt.W3cSubresourceIntegrityMetadata
import io.ktor.client.HttpClient
import kotlinx.serialization.json.Json
import kotlin.time.Clock

/**
* A [CredentialMetadataRegistry] that fetches metadata documents over HTTP, mirroring
* [at.asitplus.wallet.lib.data.StaticCredentialMetadataRegistry] but with remote retrieval.
*
* This registry **owns** the `vct -> URL` mapping ([documentUrls]); the underlying
* [KtorSdJwtTypeMetadataDocumentRetriever] is given only a lookup (`documentUrls::get`), so the mapping stays in the
* registry, not in the retriever. The same URL is reused as the resolved scheme's `schemaUri`.
*
* [documentUrls] is intentionally minimal — fill it with the known `vct`/URL pairs. The map is shared with the
* retriever, so later additions are picked up for both lookup and retrieval (incl. `extends` parents).
*
* Identifier resolution mirrors the static registry: an [aliases] entry wins; otherwise for [SD_JWT] the identifier is
* the `vct` directly. Mapping a decoupled W3C `vcType` / ISO `docType` to its `vct` needs an [aliases] entry until
* richer discovery is added.
*/
class RemoteCredentialMetadataRegistry(
httpClient: HttpClient,
clock: Clock,
/** `vct` -> hosted document URL. Owned here; fill in the known pairs. */
val documentUrls: MutableMap<SdJwtVcType, String> = mutableMapOf(),
private val aliases: Map<CredentialMetadataLookup, SdJwtVcType> = emptyMap(),
private val integrityMetadata: Map<SdJwtVcType, W3cSubresourceIntegrityMetadata> = emptyMap(),
json: Json = Json.Default,
integrityChecker: SdJwtTypeMetadataDocumentIntegrityChecker = SdJwtTypeMetadataDocumentIntegrityChecker.DEFAULT,
) : CredentialMetadataRegistry {

private val resolver = DelegatingSdJwtTypeMetadataDocumentResolver(
documentRetriever = KtorSdJwtTypeMetadataDocumentRetriever(
httpClient = httpClient,
clock = clock,
locateUrl = documentUrls::get,
json = json,
integrityChecker = integrityChecker,
),
integrityChecker = integrityChecker,
)

override suspend fun findEntry(
identifier: String,
representation: CredentialRepresentation,
): ResolvedCredentialMetadata? {
val vct = aliases[CredentialMetadataLookup(representation, identifier)]
?: SdJwtVcType(identifier).takeIf { representation == SD_JWT && documentUrls.containsKey(it) }
?: return null
val loadedFrom = documentUrls[vct] ?: return null
// Return null on fetch/integrity failure so AttributeIndex can fall back to a fallback scheme.
val metadata = catching { resolver.resolve(vct, integrityMetadata[vct]) }.getOrNull() ?: return null
return ResolvedCredentialMetadata(
metadata = metadata,
loadedFrom = loadedFrom,
aliases = aliases.entries.filter { it.value == vct }.map { it.key.identifier }.toSet(),
)
}
}
40 changes: 37 additions & 3 deletions vck-openid-ktor/src/commonTest/kotlin/TestConfig.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import at.asitplus.testballoon.matrix.ExecutionMode
import at.asitplus.testballoon.matrix.MatrixTestDefaults
import at.asitplus.wallet.eupid.EuPidItemValueSerializerMap
import at.asitplus.wallet.eupid.EuPidJsonValueEncoder
import at.asitplus.wallet.eupid.EuPidMetadataDocument
import at.asitplus.wallet.eupidsdjwt.EuPidSdJwtMetadataDocument
import at.asitplus.wallet.lib.LibraryInitializer
import at.asitplus.wallet.lib.data.StaticCredentialMetadataRegistry
import at.asitplus.wallet.mdl.MobileDrivingLicenceItemValueSerializerMap
import at.asitplus.wallet.mdl.MobileDrivingLicenceJsonValueEncoder
import at.asitplus.wallet.mdl.MobileDrivingLicenceMetadataDocument
import at.asitplus.wallet.sdjwt.SdJwtTypeMetadataDocumentRegistry
import at.asitplus.wallet.sdjwt.SdJwtVcType
import de.infix.testBalloon.framework.core.TestSession
import io.github.aakira.napier.DebugAntilog
import io.github.aakira.napier.Napier
Expand All @@ -10,8 +21,31 @@ class TestConfig : TestSession(
init {
Napier.takeLogarithm()
Napier.base(DebugAntilog())
at.asitplus.wallet.eupid.Initializer.initWithVCK()
at.asitplus.wallet.eupidsdjwt.Initializer.initWithVCK()
at.asitplus.wallet.mdl.Initializer.initWithVCK()

LibraryInitializer.registerCredentialMetadataRegistry(
StaticCredentialMetadataRegistry(
documentRegistry = SdJwtTypeMetadataDocumentRegistry(
EuPidSdJwtMetadataDocument,
EuPidMetadataDocument,
MobileDrivingLicenceMetadataDocument,
),
documentUrls = mapOf(
SdJwtVcType(EuPidSdJwtMetadataDocument.first.string) to "https://example.com",
SdJwtVcType(EuPidMetadataDocument.first.string) to "https://example.com",
SdJwtVcType(MobileDrivingLicenceMetadataDocument.first.string) to "https://example.com",
)
)
)

LibraryInitializer.registerCredentialSerializers(
jsonValueEncoder = MobileDrivingLicenceJsonValueEncoder,
itemValueSerializerMap = MobileDrivingLicenceItemValueSerializerMap
)
LibraryInitializer.registerCredentialSerializers(
jsonValueEncoder = EuPidJsonValueEncoder,
itemValueSerializerMap = EuPidItemValueSerializerMap,
)
}
}


Loading
Loading