diff --git a/CHANGELOG.md b/CHANGELOG.md index ebaa09229..6a62ed12a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ Release 3.2.0 - Add function `issueCredential(CryptoPublicKey, Collection, CredentialRepresentation)` to interface `Issuer` and its implementation `IssuerAgent` - Remove function `getCredentialWithType(String, CryptoPublicKey?, Collection, CredentialRepresentation` from interface `IssuerCredentialDataProvider` - Add function `getCredential(CryptoPublicKey, CredentialScheme, CredentialRepresentation)` to interface `IssuerCredentialDataProvider` + - Refactor function `storeGetNextIndex()` in `IssuerCredentialStore` to accomodate all types of credentials Release 3.1.0 - Support representing credentials in [SD-JWT](https://drafts.oauth.net/oauth-selective-disclosure-jwt/draft-ietf-oauth-selective-disclosure-jwt.html) format diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InMemoryIssuerCredentialStore.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InMemoryIssuerCredentialStore.kt index 7a327a8dd..c42a2d9c5 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InMemoryIssuerCredentialStore.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/InMemoryIssuerCredentialStore.kt @@ -1,13 +1,11 @@ package at.asitplus.wallet.lib.agent -import at.asitplus.wallet.lib.data.CredentialSubject -import at.asitplus.wallet.lib.iso.IssuerSignedItem +import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.iso.sha256 import io.matthewnelson.encoding.base16.Base16 import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString import kotlinx.datetime.Instant - class InMemoryIssuerCredentialStore : IssuerCredentialStore { data class Credential( @@ -20,32 +18,21 @@ class InMemoryIssuerCredentialStore : IssuerCredentialStore { private val map = mutableMapOf>() override fun storeGetNextIndex( - vcId: String, - credentialSubject: CredentialSubject, + credential: IssuerCredentialStore.Credential, + subjectPublicKey: CryptoPublicKey, issuanceDate: Instant, expirationDate: Instant, timePeriod: Int ): Long { val list = map.getOrPut(timePeriod) { mutableListOf() } val newIndex = (list.maxOfOrNull { it.statusListIndex } ?: 0) + 1 - list += Credential( - vcId = vcId, - statusListIndex = newIndex, - revoked = false, - expirationDate = expirationDate - ) - return newIndex - } + val vcId = when (credential) { + is IssuerCredentialStore.Credential.Iso -> credential.issuerSignedItemList.toString().encodeToByteArray() + .sha256().encodeToString(Base16(strict = true)) - override fun storeGetNextIndex( - vcId: String, - subjectId: String, - issuanceDate: Instant, - expirationDate: Instant, - timePeriod: Int - ): Long { - val list = map.getOrPut(timePeriod) { mutableListOf() } - val newIndex = (list.maxOfOrNull { it.statusListIndex } ?: 0) + 1 + is IssuerCredentialStore.Credential.VcJwt -> credential.vcId + is IssuerCredentialStore.Credential.VcSd -> credential.vcId + } list += Credential( vcId = vcId, statusListIndex = newIndex, @@ -55,29 +42,12 @@ class InMemoryIssuerCredentialStore : IssuerCredentialStore { return newIndex } - override fun storeGetNextIndex( - issuerSignedItemList: List, - issuanceDate: Instant, - expirationDate: Instant, - timePeriod: Int - ): Long { - val list = map.getOrPut(timePeriod) { mutableListOf() } - val newIndex = (list.maxOfOrNull { it.statusListIndex } ?: 0) + 1 - list += Credential( - vcId = issuerSignedItemList.toString().encodeToByteArray().sha256().encodeToString(Base16(strict = true)), - statusListIndex = newIndex, - revoked = false, - expirationDate = expirationDate - ) - return newIndex - } - override fun getRevokedStatusListIndexList(timePeriod: Int): Collection { - return map.getOrPut(timePeriod) { mutableListOf() }.filter { it.revoked }.map { it.statusListIndex } + return map.getOrPut(timePeriod) { mutableListOf() }.filter { it.revoked }.map { it.statusListIndex } } override fun revoke(vcId: String, timePeriod: Int): Boolean { - val entry = map.getOrPut(timePeriod) { mutableListOf() }.find { it.vcId == vcId } ?: return false + val entry = map.getOrPut(timePeriod) { mutableListOf() }.find { it.vcId == vcId } ?: return false entry.revoked = true return true } diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt index ff5022896..ed897595d 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerAgent.kt @@ -70,8 +70,8 @@ class IssuerAgent( dataProvider: IssuerCredentialDataProvider = EmptyCredentialDataProvider, ): IssuerAgent = IssuerAgent( validator = Validator.newDefaultInstance( - verifierCryptoService, - Parser(clock.now().toEpochMilliseconds()) + cryptoService = verifierCryptoService, + parser = Parser(clock.now().toEpochMilliseconds()) ), issuerCredentialStore = issuerCredentialStore, jwsService = DefaultJwsService(cryptoService), @@ -102,11 +102,11 @@ class IssuerAgent( continue } dataProvider.getCredential(subjectPublicKey, scheme, representation).fold( - onSuccess = { - it.forEach { credentialToBeIssued -> - issueCredential(credentialToBeIssued, subjectPublicKey, scheme).also { - failed += it.failed - successful += it.successful + onSuccess = { toBeIssued -> + toBeIssued.forEach { credentialToBeIssued -> + issueCredential(credentialToBeIssued, subjectPublicKey, scheme).also { result -> + failed += result.failed + successful += result.successful } } }, @@ -124,21 +124,10 @@ class IssuerAgent( credential: CredentialToBeIssued, subjectPublicKey: CryptoPublicKey, scheme: ConstantIndex.CredentialScheme, - ): Issuer.IssuedCredentialResult { - val issuanceDate = clock.now() - when (credential) { - is CredentialToBeIssued.Iso -> { - return issueMdoc(credential, scheme, subjectPublicKey, issuanceDate) - } - - is CredentialToBeIssued.VcJwt -> { - return issueVc(credential, scheme, issuanceDate) - } - - is CredentialToBeIssued.VcSd -> { - return issueVcSd(credential, scheme, subjectPublicKey, issuanceDate) - } - } + ): Issuer.IssuedCredentialResult = when (credential) { + is CredentialToBeIssued.Iso -> issueMdoc(credential, scheme, subjectPublicKey, clock.now()) + is CredentialToBeIssued.VcJwt -> issueVc(credential, scheme, subjectPublicKey, clock.now()) + is CredentialToBeIssued.VcSd -> issueVcSd(credential, scheme, subjectPublicKey, clock.now()) } private suspend fun issueMdoc( @@ -148,18 +137,16 @@ class IssuerAgent( issuanceDate: Instant ): Issuer.IssuedCredentialResult { val expirationDate = credential.expiration - //TODO: we are not going to store anything ATM since we will first need a clear stance on what to store - /*val timePeriod = timePeriodProvider.getTimePeriodFor(issuanceDate) - val statusListIndex = issuerCredentialStore.storeGetNextIndex( - credential.issuerSignedItems, - issuanceDate, - expirationDate, - timePeriod, - ) ?: return Issuer.IssuedCredentialResult( - failed = listOf( - Issuer.FailedAttribute(credential.attributeType, DataSourceProblem("no statusListIndex")) - ) - ).also { Napier.w("Got no statusListIndex from issuerCredentialStore, can't issue credential") }*/ + val timePeriod = timePeriodProvider.getTimePeriodFor(issuanceDate) + issuerCredentialStore.storeGetNextIndex( + credential = IssuerCredentialStore.Credential.Iso(credential.issuerSignedItems), + subjectPublicKey = subjectPublicKey, + issuanceDate = issuanceDate, + expirationDate = expirationDate, + timePeriod = timePeriod, + ) ?: return Issuer.IssuedCredentialResult( + failed = listOf(Issuer.FailedAttribute(scheme.vcType, DataSourceProblem("vcId internal mismatch"))) + ).also { Napier.w("Got no statusListIndex from issuerCredentialStore, can't issue credential") } val mso = MobileSecurityObject( version = VERSION_1_0, digestAlgorithm = DIGEST_SHA_256, @@ -188,29 +175,26 @@ class IssuerAgent( addCertificate = true, ).getOrThrow() ) - return Issuer.IssuedCredentialResult( - successful = listOf(Issuer.IssuedCredential.Iso(issuerSigned, scheme)) - ) + return Issuer.IssuedCredentialResult(successful = listOf(Issuer.IssuedCredential.Iso(issuerSigned, scheme))) } private suspend fun issueVc( credential: CredentialToBeIssued.VcJwt, scheme: ConstantIndex.CredentialScheme, - issuanceDate: Instant + subjectPublicKey: CryptoPublicKey, + issuanceDate: Instant, ): Issuer.IssuedCredentialResult { val vcId = "urn:uuid:${uuid4()}" val expirationDate = credential.expiration val timePeriod = timePeriodProvider.getTimePeriodFor(issuanceDate) val statusListIndex = issuerCredentialStore.storeGetNextIndex( - vcId, - credential.subject, - issuanceDate, - expirationDate, - timePeriod + credential = IssuerCredentialStore.Credential.VcJwt(vcId, credential.subject), + subjectPublicKey = subjectPublicKey, + issuanceDate = issuanceDate, + expirationDate = expirationDate, + timePeriod = timePeriod ) ?: return Issuer.IssuedCredentialResult( - failed = listOf( - Issuer.FailedAttribute(scheme.vcType, DataSourceProblem("vcId internal mismatch")) - ) + failed = listOf(Issuer.FailedAttribute(scheme.vcType, DataSourceProblem("vcId internal mismatch"))) ).also { Napier.w("Got no statusListIndex from issuerCredentialStore, can't issue credential") } val credentialStatus = CredentialStatus(getRevocationListUrlFor(timePeriod), statusListIndex) @@ -226,9 +210,7 @@ class IssuerAgent( val vcInJws = wrapVcInJws(vc) ?: return Issuer.IssuedCredentialResult( - failed = listOf( - Issuer.FailedAttribute(scheme.vcType, RuntimeException("signing failed")) - ) + failed = listOf(Issuer.FailedAttribute(scheme.vcType, RuntimeException("signing failed"))) ).also { Napier.w("Could not wrap credential in JWS") } return Issuer.IssuedCredentialResult( successful = listOf( @@ -252,15 +234,13 @@ class IssuerAgent( val timePeriod = timePeriodProvider.getTimePeriodFor(issuanceDate) val subjectId = subjectPublicKey.toJsonWebKey().identifier val statusListIndex = issuerCredentialStore.storeGetNextIndex( - vcId, - subjectId, - issuanceDate, - expirationDate, - timePeriod + credential = IssuerCredentialStore.Credential.VcSd(vcId, credential.claims), + subjectPublicKey = subjectPublicKey, + issuanceDate = issuanceDate, + expirationDate = expirationDate, + timePeriod = timePeriod ) ?: return Issuer.IssuedCredentialResult( - failed = listOf( - Issuer.FailedAttribute(scheme.vcType, DataSourceProblem("vcId internal mismatch")) - ) + failed = listOf(Issuer.FailedAttribute(scheme.vcType, DataSourceProblem("vcId internal mismatch"))) ).also { Napier.w("Got no statusListIndex from issuerCredentialStore, can't issue credential") } val credentialStatus = CredentialStatus(getRevocationListUrlFor(timePeriod), statusListIndex) @@ -285,9 +265,7 @@ class IssuerAgent( // TODO Which content type to use for SD-JWT inside an JWS? val jws = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload) ?: return Issuer.IssuedCredentialResult( - failed = listOf( - Issuer.FailedAttribute(scheme.vcType, RuntimeException("signing failed")) - ) + failed = listOf(Issuer.FailedAttribute(scheme.vcType, RuntimeException("signing failed"))) ).also { Napier.w("Could not wrap credential in SD-JWT") } val vcInSdJwt = (listOf(jws) + disclosures).joinToString("~") @@ -301,7 +279,8 @@ class IssuerAgent( * returns a JWS representation of that. */ override suspend fun issueRevocationListCredential(timePeriod: Int?): String? { - val revocationListUrl = getRevocationListUrlFor(timePeriod ?: timePeriodProvider.getCurrentTimePeriod(clock)) + val revocationListUrl = + getRevocationListUrlFor(timePeriod ?: timePeriodProvider.getCurrentTimePeriod(clock)) val revocationList = buildRevocationList(timePeriod ?: timePeriodProvider.getCurrentTimePeriod(clock)) ?: return null val subject = RevocationListSubject("$revocationListUrl#list", revocationList) diff --git a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerCredentialStore.kt b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerCredentialStore.kt index 6f500120a..361d6c2e0 100644 --- a/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerCredentialStore.kt +++ b/vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/IssuerCredentialStore.kt @@ -1,5 +1,6 @@ package at.asitplus.wallet.lib.agent +import at.asitplus.wallet.lib.CryptoPublicKey import at.asitplus.wallet.lib.data.CredentialSubject import at.asitplus.wallet.lib.iso.IssuerSignedItem import kotlinx.datetime.Instant @@ -9,18 +10,11 @@ import kotlinx.datetime.Instant */ interface IssuerCredentialStore { - /** - * Called by the issuer when creating a new credential. - * Expected to return a new index to use as a `statusListIndex` - * Returns null if `vcId` is already registered - */ - fun storeGetNextIndex( - vcId: String, - credentialSubject: CredentialSubject, - issuanceDate: Instant, - expirationDate: Instant, - timePeriod: Int - ): Long? + sealed class Credential { + data class VcJwt(val vcId: String, val credentialSubject: CredentialSubject) : Credential() + data class VcSd(val vcId: String, val claims: Collection) : Credential() + data class Iso(val issuerSignedItemList: List) : Credential() + } /** * Called by the issuer when creating a new credential. @@ -28,25 +22,13 @@ interface IssuerCredentialStore { * Returns null if `vcId` is already registered */ fun storeGetNextIndex( - vcId: String, - subjectId: String, + credential: Credential, + subjectPublicKey: CryptoPublicKey, issuanceDate: Instant, expirationDate: Instant, timePeriod: Int ): Long? - /** - * Called by the issuer when creating a new credential. - * Expected to return a new index to use as something for ISO revocation?! - * Returns null if `vcId` is already registered - */ - fun storeGetNextIndex( - issuerSignedItemList: List, - issuanceDate: Instant, - expirationDate: Instant, - timePeriod: Int, - ): Long? - /** * Returns a list of revoked credentials, represented by their `statusListIndex` */ diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentRevocationTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentRevocationTest.kt index 0020f11a9..f15049d54 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentRevocationTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/AgentRevocationTest.kt @@ -131,8 +131,13 @@ private fun IssuerCredentialStore.revokeCredentialsWithIndexes(revokedIndexes: L val expirationDate = issuanceDate + 60.seconds for (i in 1..16) { val vcId = uuid4().toString() - val revListIndex = - storeGetNextIndex(vcId, cred, issuanceDate, expirationDate, FixedTimePeriodProvider.timePeriod)!! + val revListIndex = storeGetNextIndex( + credential = IssuerCredentialStore.Credential.VcJwt(vcId, cred), + subjectPublicKey = DefaultCryptoService().toPublicKey(), + issuanceDate = issuanceDate, + expirationDate = expirationDate, + timePeriod = FixedTimePeriodProvider.timePeriod + )!! if (revokedIndexes.contains(revListIndex)) { revoke(vcId, FixedTimePeriodProvider.timePeriod) } @@ -146,8 +151,13 @@ private fun IssuerCredentialStore.revokeRandomCredentials(): MutableList { val expirationDate = issuanceDate + 60.seconds for (i in 1..256) { val vcId = uuid4().toString() - val revListIndex = - storeGetNextIndex(vcId, cred, issuanceDate, expirationDate, FixedTimePeriodProvider.timePeriod)!! + val revListIndex = storeGetNextIndex( + credential = IssuerCredentialStore.Credential.VcJwt(vcId, cred), + subjectPublicKey = DefaultCryptoService().toPublicKey(), + issuanceDate = issuanceDate, + expirationDate = expirationDate, + timePeriod = FixedTimePeriodProvider.timePeriod + )!! if (Random.nextBoolean()) { expectedRevocationList += revListIndex revoke(vcId, FixedTimePeriodProvider.timePeriod) diff --git a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt index c5097d321..556b50801 100644 --- a/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt +++ b/vclib/src/commonTest/kotlin/at/asitplus/wallet/lib/agent/ValidatorVcTest.kt @@ -384,14 +384,13 @@ class ValidatorVcTest : FreeSpec() { sub as AtomicAttribute2023 val vcId = "urn:uuid:${uuid4()}" val exp = expirationDate ?: (Clock.System.now() + 60.seconds) - val statusListIndex = - issuerCredentialStore.storeGetNextIndex( - vcId, - sub, - issuanceDate, - exp, - FixedTimePeriodProvider.timePeriod - )!! + val statusListIndex = issuerCredentialStore.storeGetNextIndex( + credential = IssuerCredentialStore.Credential.VcJwt(vcId, sub), + subjectPublicKey = issuerCryptoService.toPublicKey(), + issuanceDate = issuanceDate, + expirationDate = exp, + timePeriod = FixedTimePeriodProvider.timePeriod + )!! val credentialStatus = CredentialStatus(revocationListUrl, statusListIndex) return VerifiableCredential( id = vcId,