Skip to content

Commit

Permalink
Refactor storing issued credentials in issuer store
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Oct 24, 2023
1 parent a785834 commit 907fb6d
Show file tree
Hide file tree
Showing 6 changed files with 81 additions and 140 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Release 3.2.0
- Add function `issueCredential(CryptoPublicKey, Collection<String>, CredentialRepresentation)` to interface `Issuer` and its implementation `IssuerAgent`
- Remove function `getCredentialWithType(String, CryptoPublicKey?, Collection<String>, 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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(
Expand All @@ -20,32 +18,21 @@ class InMemoryIssuerCredentialStore : IssuerCredentialStore {
private val map = mutableMapOf<Int, MutableList<Credential>>()

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,
Expand All @@ -55,29 +42,12 @@ class InMemoryIssuerCredentialStore : IssuerCredentialStore {
return newIndex
}

override fun storeGetNextIndex(
issuerSignedItemList: List<IssuerSignedItem>,
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<Long> {
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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
}
}
},
Expand All @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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)
Expand All @@ -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(
Expand All @@ -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)

Expand All @@ -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("~")

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,44 +10,25 @@ 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<ClaimToBeIssued>) : Credential()
data class Iso(val issuerSignedItemList: List<IssuerSignedItem>) : Credential()
}

/**
* 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,
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<IssuerSignedItem>,
issuanceDate: Instant,
expirationDate: Instant,
timePeriod: Int,
): Long?

/**
* Returns a list of revoked credentials, represented by their `statusListIndex`
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -146,8 +151,13 @@ private fun IssuerCredentialStore.revokeRandomCredentials(): MutableList<Long> {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit 907fb6d

Please sign in to comment.