Skip to content

Commit

Permalink
Issue, store and verify SD-JWT credentials correctly
Browse files Browse the repository at this point in the history
  • Loading branch information
nodh committed Sep 25, 2023
1 parent cf1cb33 commit 870ad2f
Show file tree
Hide file tree
Showing 13 changed files with 300 additions and 109 deletions.
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.data.SelectiveDisclosureItem
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
import at.asitplus.wallet.lib.data.VerifiableCredentialSdJwt
import at.asitplus.wallet.lib.data.VerifiablePresentation
Expand Down Expand Up @@ -134,6 +135,13 @@ interface Holder {
*/
data class Signed(val jws: String) : CreatePresentationResult()

/**
* [vpJws] contains a valid, serialized, Verifiable Presentation containing an SD-JWT credential,
* that can be parsed by [Verifier.verifyPresentation].
* [disclosures] contains the concrete values for disclosed claims.
*/
data class SdJwt(val vpJws: String, val disclosures: List<SelectiveDisclosureItem>) : CreatePresentationResult()

/**
* [document] contains a valid ISO 18013 [Document] with [IssuerSigned] and [DeviceSigned] structures
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import at.asitplus.wallet.lib.cbor.CoseAlgorithm
import at.asitplus.wallet.lib.cbor.CoseHeader
import at.asitplus.wallet.lib.cbor.CoseService
import at.asitplus.wallet.lib.cbor.DefaultCoseService
import at.asitplus.wallet.lib.data.SelectiveDisclosureItem
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
import at.asitplus.wallet.lib.data.VerifiableCredentialSdJwt
import at.asitplus.wallet.lib.data.VerifiablePresentation
Expand Down Expand Up @@ -104,7 +105,7 @@ class HolderAgent(
is Verifier.VerifyCredentialResult.InvalidStructure -> rejected += vc.input
is Verifier.VerifyCredentialResult.Revoked -> rejected += vc.input
is Verifier.VerifyCredentialResult.SuccessSdJwt -> acceptedSdJwt += vc.sdJwt
.also { subjectCredentialStore.storeCredentialSd(it, cred.vcSdJwt) }
.also { subjectCredentialStore.storeCredentialSd(it, cred.vcSdJwt, vc.disclosures) }

else -> {}
}
Expand Down Expand Up @@ -173,7 +174,7 @@ class HolderAgent(

/**
* Creates a [VerifiablePresentation] serialized as a JWT for all the credentials we have stored,
* that match [attributeTypes] (if specified). Optionally filters by [requestedClaims] (e.g. in ISO case).
* that match [attributeTypes] (if specified). Optionally filters by [requestedClaims] (e.g. in ISO or SD-JWT case).
*
* May return null if no valid credentials (i.e. non-revoked, matching attribute name) are available.
*/
Expand Down Expand Up @@ -227,10 +228,31 @@ class HolderAgent(
)
)
}
val validSdJwtCredentials = credentials
.filterIsInstance<SubjectCredentialStore.StoreEntry.SdJwt>()
// TODO Revocation check for SD-JWT
if (validSdJwtCredentials.isNotEmpty()) {
val vp = VerifiablePresentation(validSdJwtCredentials.map { it.vcSerialized }.toTypedArray())
val vpSerialized = vp.toJws(challenge, identifier, audienceId).serialize()
val jwsPayload = vpSerialized.encodeToByteArray()
val jws = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload)
?: return null
.also { Napier.w("Could not create JWS for presentation") }
val disclosures = validSdJwtCredentials.flatMap { it.disclosures }
.filter { it.discloseItem(requestedClaims) }
return Holder.CreatePresentationResult.SdJwt(jws, disclosures)
}
Napier.w("Got no valid credentials for $attributeTypes")
return null
}

private fun SelectiveDisclosureItem.discloseItem(requestedClaims: Collection<String>?) =
if (requestedClaims?.isNotEmpty() == true) {
claimName in requestedClaims
} else {
true
}

private fun ByteStringWrapper<IssuerSignedItem>.discloseItem(requestedClaims: Collection<String>?) =
if (requestedClaims?.isNotEmpty() == true) {
value.elementIdentifier in requestedClaims
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package at.asitplus.wallet.lib.agent

import at.asitplus.KmmResult
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.SelectiveDisclosureItem
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
import at.asitplus.wallet.lib.data.VerifiableCredentialSdJwt
import at.asitplus.wallet.lib.iso.IssuerSigned
Expand All @@ -15,8 +16,12 @@ class InMemorySubjectCredentialStore : SubjectCredentialStore {
credentials += SubjectCredentialStore.StoreEntry.Vc(vcSerialized, vc)
}

override suspend fun storeCredentialSd(vc: VerifiableCredentialSdJwt, vcSerialized: String) {
credentials += SubjectCredentialStore.StoreEntry.SdJwt(vcSerialized, vc)
override suspend fun storeCredentialSd(
vc: VerifiableCredentialSdJwt,
vcSerialized: String,
disclosures: List<SelectiveDisclosureItem>
) {
credentials += SubjectCredentialStore.StoreEntry.SdJwt(vcSerialized, vc, disclosures)
}

override suspend fun storeCredential(issuerSigned: IssuerSigned) {
Expand All @@ -39,7 +44,7 @@ class InMemorySubjectCredentialStore : SubjectCredentialStore {
when (this) {
is SubjectCredentialStore.StoreEntry.Iso -> ConstantIndex.MobileDrivingLicence2023.vcType in requiredAttributeTypes
is SubjectCredentialStore.StoreEntry.Vc -> vc.vc.type.any { it in requiredAttributeTypes }
is SubjectCredentialStore.StoreEntry.SdJwt -> sdJwt.vc.type.any { it in requiredAttributeTypes }
is SubjectCredentialStore.StoreEntry.SdJwt -> sdJwt.type.any { it in requiredAttributeTypes }
}
} else true

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import at.asitplus.wallet.lib.cbor.CoseKey
import at.asitplus.wallet.lib.cbor.CoseService
import at.asitplus.wallet.lib.cbor.DefaultCoseService
import at.asitplus.wallet.lib.data.Base64Strict
import at.asitplus.wallet.lib.data.Base64UrlStrict
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.CredentialStatus
import at.asitplus.wallet.lib.data.RevocationListSubject
Expand All @@ -31,11 +32,14 @@ import at.asitplus.wallet.lib.iso.ValueDigestList
import at.asitplus.wallet.lib.jws.DefaultJwsService
import at.asitplus.wallet.lib.jws.JwsContentTypeConstants
import at.asitplus.wallet.lib.jws.JwsService
import at.asitplus.wallet.lib.jws.JwsSigned
import at.asitplus.wallet.lib.jws.SdJwtSigned
import com.benasher44.uuid.uuid4
import io.github.aakira.napier.Napier
import io.matthewnelson.encoding.core.Encoder.Companion.encodeToString
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import okio.ByteString.Companion.toByteString
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
Expand Down Expand Up @@ -95,7 +99,7 @@ class IssuerAgent(
result.exceptionOrNull()?.let { failure ->
return Issuer.IssuedCredentialResult(failed = attributeTypes.map { Issuer.FailedAttribute(it, failure) })
}
val issuedCredentials = result.getOrThrow().map { issueCredential(it) }
val issuedCredentials = result.getOrThrow().map { issueCredential(it, representation) }
return Issuer.IssuedCredentialResult(
successful = issuedCredentials.flatMap { it.successful },
failed = issuedCredentials.flatMap { it.failed })
Expand Down Expand Up @@ -203,7 +207,6 @@ class IssuerAgent(
Issuer.FailedAttribute(credential.attributeType, RuntimeException("signing failed"))
)
).also { Napier.w("Could not wrap credential in JWS") }

return Issuer.IssuedCredentialResult(
successful = listOf(Issuer.IssuedCredential.VcJwt(vcInJws, credential.attachments))
)
Expand All @@ -216,8 +219,6 @@ class IssuerAgent(
Issuer.FailedAttribute(credential.attributeType, RuntimeException("signing failed"))
)
).also { Napier.w("Could not wrap credential in SD-JWT") }
// TODO When to serialize SD-JWT with disclosures appended

return Issuer.IssuedCredentialResult(
successful = listOf(Issuer.IssuedCredential.VcSdJwt(vcInSdJwt))
)
Expand Down Expand Up @@ -288,17 +289,26 @@ class IssuerAgent(

private suspend fun wrapVcInSdJwt(vc: VerifiableCredential): String? {
val claims = vc.credentialSubject.getClaims()
val disclosures = claims
.map { SelectiveDisclosureItem(Random.nextBytes(32), it.name, it.value) }
val disclosureDigests = disclosures
.map { it.serialize() }
.map { it.encodeToByteArray().encodeToString(Base64UrlStrict) }
.map { it.encodeToByteArray().toByteString().sha256().base64Url() }
val jwsPayload = VerifiableCredentialSdJwt(
vc = vc, // TODO Decide on correct integration with VC
subject = vc.credentialSubject.id,
notBefore = vc.issuanceDate,
issuer = vc.issuer,
expiration = vc.expirationDate,
jwtId = vc.id,
selectiveDisclosures = claims.map { SelectiveDisclosureItem(Random.nextBytes(32), it.name, it.value) }
disclosureDigests = disclosureDigests,
type = vc.type,
selectiveDisclosureAlgorithm = "sha-256",
).serialize().encodeToByteArray()
// TODO Which content type to use for SD-JWT inside an JWS?
return jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload)
val jws = jwsService.createSignedJwt(JwsContentTypeConstants.JWT, jwsPayload)
?: return null
return SdJwtSigned(JwsSigned.parse(jws)!!, disclosures).serialize()
}

private fun getRevocationListUrlFor(timePeriod: Int) =
Expand Down
26 changes: 2 additions & 24 deletions vclib/src/commonMain/kotlin/at/asitplus/wallet/lib/agent/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -121,41 +121,19 @@ class Parser(
* @param it the JWS enclosing the VC, in compact representation
*/
fun parseSdJwt(it: String, sdJwt: VerifiableCredentialSdJwt, kid: String? = null): ParseVcResult {
// TODO Unify with parseVcJws
if (kid != null && sdJwt.issuer != kid)
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("iss invalid") }
if (sdJwt.issuer != sdJwt.vc.issuer)
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("iss invalid") }
if (sdJwt.jwtId != sdJwt.vc.id)
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("jti invalid") }
if (sdJwt.subject != sdJwt.vc.credentialSubject.id)
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("sub invalid") }
if (!sdJwt.vc.type.contains(VERIFIABLE_CREDENTIAL))
if (!sdJwt.type.contains(VERIFIABLE_CREDENTIAL))
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("type invalid") }
if (sdJwt.expiration != null && sdJwt.expiration < (clock.now() - timeLeeway))
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("exp invalid") }
if (sdJwt.vc.expirationDate != null && sdJwt.vc.expirationDate < (clock.now() - timeLeeway))
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("expirationDate invalid") }
if (sdJwt.expiration?.epochSeconds != sdJwt.vc.expirationDate?.epochSeconds)
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("exp invalid") }
if (sdJwt.notBefore > (clock.now() + timeLeeway))
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("nbf invalid") }
if (sdJwt.vc.issuanceDate > (clock.now() + timeLeeway))
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("issuanceDate invalid") }
if (sdJwt.notBefore.epochSeconds != sdJwt.vc.issuanceDate.epochSeconds)
return ParseVcResult.InvalidStructure(it)
.also { Napier.d("nbf invalid") }
Napier.d("VC is valid")
Napier.d("SD-JWT is valid")
return ParseVcResult.SuccessSdJwt(sdJwt)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package at.asitplus.wallet.lib.agent

import at.asitplus.KmmResult
import at.asitplus.wallet.lib.data.SelectiveDisclosureItem
import at.asitplus.wallet.lib.data.VerifiableCredential
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
import at.asitplus.wallet.lib.data.VerifiableCredentialSdJwt
Expand All @@ -27,7 +28,11 @@ interface SubjectCredentialStore {
* @param vc Instance of [VerifiableCredentialSdJwt]
* @param vcSerialized Serialized form of [VerifiableCredential]
*/
suspend fun storeCredentialSd(vc: VerifiableCredentialSdJwt, vcSerialized: String)
suspend fun storeCredentialSd(
vc: VerifiableCredentialSdJwt,
vcSerialized: String,
disclosures: List<SelectiveDisclosureItem>
)

/**
* Implementations should store the passed credential in a secure way.
Expand Down Expand Up @@ -65,7 +70,12 @@ interface SubjectCredentialStore {

sealed class StoreEntry {
data class Vc(val vcSerialized: String, val vc: VerifiableCredentialJws) : StoreEntry()
data class SdJwt(val vcSerialized: String, val sdJwt: VerifiableCredentialSdJwt) : StoreEntry()
data class SdJwt(
val vcSerialized: String,
val sdJwt: VerifiableCredentialSdJwt,
val disclosures: List<SelectiveDisclosureItem>
) : StoreEntry()

data class Iso(val issuerSigned: IssuerSigned) : StoreEntry()
}

Expand Down
Loading

0 comments on commit 870ad2f

Please sign in to comment.