Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SD-JWT: Refactor interface #154

Merged
merged 5 commits into from
Oct 30, 2024
Merged
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ Release 5.1.0:
- `WalletService` supports building multiple authorization details to request a token for more than one credential
- Remove `buildAuthorizationDetails(RequestOptions)` for `WalletService`, please migrate to `buildScope(RequestOptions)`
- Note that multiple `scope` values may be joined with a whitespace ` `
- SD-JWT:
- Pass around decoded data with `SdJwtSigned` in several result classes like `VerifyPresentationResult.SuccessSdJwt`
- Rename `disclosures` to `reconstructed` in several result classes like `AuthnResponseResult.SuccessSdJwt`
- Correctly implement confirmation claim in `VerifiableCredentialSdJwt`, migrating from `JsonWebKey` to `ConfirmationClaim`

Release 5.0.1:
- Update JsonPath4K to 2.4.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ import at.asitplus.wallet.lib.agent.*
import at.asitplus.wallet.lib.data.*
import at.asitplus.wallet.lib.data.ConstantIndex.supportsSdJwt
import at.asitplus.wallet.lib.data.ConstantIndex.supportsVcJwt
import at.asitplus.wallet.lib.jws.DefaultJwsService
import at.asitplus.wallet.lib.jws.DefaultVerifierJwsService
import at.asitplus.wallet.lib.jws.JwsService
import at.asitplus.wallet.lib.jws.VerifierJwsService
import at.asitplus.wallet.lib.jws.*
import at.asitplus.wallet.lib.oidvci.*
import com.benasher44.uuid.uuid4
import io.github.aakira.napier.Napier
Expand Down Expand Up @@ -503,9 +500,12 @@ class OidcSiopVerifier private constructor(
* Successfully decoded and validated the response from the Wallet (W3C credential in SD-JWT)
*/
data class SuccessSdJwt(
val jwsSigned: JwsSigned,
val sdJwtSigned: SdJwtSigned,
val verifiableCredentialSdJwt: VerifiableCredentialSdJwt,
@Deprecated("Renamed to verifiableCredentialSdJwt", replaceWith = ReplaceWith("verifiableCredentialSdJwt"))
val sdJwt: VerifiableCredentialSdJwt,
val disclosures: List<SelectiveDisclosureItem>,
val reconstructed: ReconstructedSdJwtClaims,
val disclosures: Collection<SelectiveDisclosureItem>,
val state: String?,
) : AuthnResponseResult()

Expand Down Expand Up @@ -672,6 +672,7 @@ class OidcSiopVerifier private constructor(
else -> throw IllegalArgumentException()
}

@Suppress("DEPRECATION")
private fun Verifier.VerifyPresentationResult.mapToAuthnResponseResult(state: String) = when (this) {
is Verifier.VerifyPresentationResult.InvalidStructure ->
AuthnResponseResult.Error("parse vp failed", state)
Expand All @@ -690,8 +691,14 @@ class OidcSiopVerifier private constructor(
.also { Napier.i("VP success: $this") }

is Verifier.VerifyPresentationResult.SuccessSdJwt ->
AuthnResponseResult.SuccessSdJwt(jwsSigned, sdJwt, disclosures, state)
.also { Napier.i("VP success: $this") }
AuthnResponseResult.SuccessSdJwt(
sdJwtSigned,
verifiableCredentialSdJwt,
sdJwt,
reconstructed,
disclosures,
state
).also { Napier.i("VP success: $this") }
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ class OidcSiopCombinedProtocolTest : FreeSpec({

val result = verifierSiop.validateAuthnResponse(authnResponse.url)
.shouldBeInstanceOf<OidcSiopVerifier.AuthnResponseResult.SuccessSdJwt>()
result.sdJwt.type?.shouldContain(ConstantIndex.AtomicAttribute2023.vcType)
result.verifiableCredentialSdJwt.type?.shouldContain(ConstantIndex.AtomicAttribute2023.vcType)
}
}

Expand Down Expand Up @@ -242,21 +242,21 @@ class OidcSiopCombinedProtocolTest : FreeSpec({
groupedResult.validationResults.size shouldBe 2
groupedResult.validationResults.forEach { result ->
result.shouldBeInstanceOf<OidcSiopVerifier.AuthnResponseResult.SuccessSdJwt>()
result.disclosures.shouldNotBeEmpty()
when (result.sdJwt.verifiableCredentialType) {
result.reconstructed.claims.shouldNotBeEmpty()
when (result.verifiableCredentialSdJwt.verifiableCredentialType) {
EuPidScheme.sdJwtType -> {
result.disclosures.firstOrNull { it.claimName == EuPidScheme.Attributes.FAMILY_NAME }
result.reconstructed.claims.firstOrNull { it.claimName == EuPidScheme.Attributes.FAMILY_NAME }
.shouldNotBeNull()
result.disclosures.firstOrNull { it.claimName == EuPidScheme.Attributes.GIVEN_NAME }
result.reconstructed.claims.firstOrNull { it.claimName == EuPidScheme.Attributes.GIVEN_NAME }
.shouldNotBeNull()
}

ConstantIndex.AtomicAttribute2023.sdJwtType -> {
result.disclosures.firstOrNull() { it.claimName == CLAIM_DATE_OF_BIRTH }.shouldNotBeNull()
result.reconstructed.claims.firstOrNull { it.claimName == CLAIM_DATE_OF_BIRTH }.shouldNotBeNull()
}

else -> {
fail("Unexpected SD-JWT type: ${result.sdJwt.verifiableCredentialType}")
fail("Unexpected SD-JWT type: ${result.verifiableCredentialSdJwt.verifiableCredentialType}")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN
import at.asitplus.wallet.lib.data.IsoDocumentParsed
import at.asitplus.wallet.lib.oidvci.formUrlEncode
import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements
import at.asitplus.wallet.mdl.MobileDrivingLicenceDataElements.FAMILY_NAME
import at.asitplus.wallet.mdl.MobileDrivingLicenceScheme
import com.benasher44.uuid.uuid4
import io.kotest.core.spec.style.FreeSpec
Expand Down Expand Up @@ -112,7 +113,7 @@ class OidcSiopIsoProtocolTest : FreeSpec({
}

"Selective Disclosure with mDL" {
val requestedClaim = MobileDrivingLicenceDataElements.FAMILY_NAME
val requestedClaim = FAMILY_NAME
verifierSiop = OidcSiopVerifier(
keyMaterial = verifierKeyMaterial,
clientIdScheme = OidcSiopVerifier.ClientIdScheme.RedirectUri(clientId),
Expand All @@ -132,14 +133,13 @@ class OidcSiopIsoProtocolTest : FreeSpec({
holderSiop,
)

document.validItems.shouldNotBeEmpty()
document.validItems.shouldBeSingleton()
document.validItems.shouldHaveSingleElement { it.elementIdentifier == requestedClaim }
document.invalidItems.shouldBeEmpty()
}

"Selective Disclosure with mDL and encryption" {
val requestedClaim = MobileDrivingLicenceDataElements.FAMILY_NAME
val requestedClaim = FAMILY_NAME
verifierSiop = OidcSiopVerifier(
keyMaterial = verifierKeyMaterial,
clientIdScheme = OidcSiopVerifier.ClientIdScheme.RedirectUri(clientId),
Expand Down Expand Up @@ -167,7 +167,6 @@ class OidcSiopIsoProtocolTest : FreeSpec({

val document = result.documents.first()

document.validItems.shouldNotBeEmpty()
document.validItems.shouldBeSingleton()
document.validItems.shouldHaveSingleElement { it.elementIdentifier == requestedClaim }
document.invalidItems.shouldBeEmpty()
Expand All @@ -186,16 +185,15 @@ class OidcSiopIsoProtocolTest : FreeSpec({
OidcSiopVerifier.RequestOptionsCredential(
MobileDrivingLicenceScheme,
ConstantIndex.CredentialRepresentation.ISO_MDOC,
listOf(MobileDrivingLicenceDataElements.FAMILY_NAME)
listOf(FAMILY_NAME)
)
)
),
holderSiop,
)

document.validItems.shouldNotBeEmpty()
document.validItems.shouldBeSingleton()
document.validItems.shouldHaveSingleElement { it.elementIdentifier == MobileDrivingLicenceDataElements.FAMILY_NAME }
document.validItems.shouldHaveSingleElement { it.elementIdentifier == FAMILY_NAME }
document.invalidItems.shouldBeEmpty()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN
import at.asitplus.wallet.lib.oidc.OidcSiopVerifier.RequestOptions
import com.benasher44.uuid.uuid4
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldBeSingleton
import io.kotest.matchers.collections.shouldHaveSingleElement
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.nulls.shouldNotBeNull
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.types.shouldBeInstanceOf
Expand Down Expand Up @@ -79,10 +77,8 @@ class OidcSiopSdJwtProtocolTest : FreeSpec({

val result = verifierSiop.validateAuthnResponse(authnResponse.url)
result.shouldBeInstanceOf<OidcSiopVerifier.AuthnResponseResult.SuccessSdJwt>()
result.sdJwt.shouldNotBeNull()
result.disclosures.shouldNotBeEmpty()
result.disclosures.shouldBeSingleton()
result.disclosures.shouldHaveSingleElement { it.claimName == requestedClaim }
result.verifiableCredentialSdJwt.shouldNotBeNull()
result.reconstructed.claims.shouldHaveSingleElement { it.claimName == requestedClaim }
}

})
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import at.asitplus.wallet.lib.data.ConstantIndex.AtomicAttribute2023.CLAIM_GIVEN
import at.asitplus.wallet.lib.oidc.OidcSiopVerifier.RequestOptions
import at.asitplus.wallet.lib.oidvci.formUrlEncode
import io.kotest.core.spec.style.FreeSpec
import io.kotest.matchers.collections.shouldNotBeEmpty
import io.kotest.matchers.collections.shouldHaveSingleElement
import io.kotest.matchers.types.shouldBeInstanceOf

class OidcSiopX509SanDnsTest : FreeSpec({
Expand Down Expand Up @@ -86,7 +86,7 @@ class OidcSiopX509SanDnsTest : FreeSpec({

val result = verifierSiop.validateAuthnResponseFromPost(authnResponse.params.formUrlEncode())
result.shouldBeInstanceOf<OidcSiopVerifier.AuthnResponseResult.SuccessSdJwt>()
result.disclosures.shouldNotBeEmpty()
result.reconstructed.claims.shouldHaveSingleElement { it.claimName == CLAIM_GIVEN_NAME }

}
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class HolderAgent(
throw VerificationError(sdJwt.toString())
}
subjectCredentialStore.storeCredential(
sdJwt.sdJwt,
sdJwt.verifiableCredentialSdJwt,
credential.vcSdJwt,
sdJwt.disclosures,
credential.scheme,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import at.asitplus.signum.indispensable.SignatureAlgorithm
import at.asitplus.signum.indispensable.cosef.toCoseKey
import at.asitplus.signum.indispensable.io.Base64Strict
import at.asitplus.signum.indispensable.io.BitSet
import at.asitplus.signum.indispensable.josef.ConfirmationClaim
import at.asitplus.signum.indispensable.josef.toJsonWebKey
import at.asitplus.wallet.lib.DataSourceProblem
import at.asitplus.wallet.lib.DefaultZlibService
Expand All @@ -19,11 +20,13 @@ import at.asitplus.wallet.lib.iso.*
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.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 kotlinx.serialization.json.encodeToJsonElement
import kotlin.random.Random
import kotlin.time.Duration
import kotlin.time.Duration.Companion.hours
Expand Down Expand Up @@ -163,11 +166,8 @@ class IssuerAgent(
) ?: throw IllegalArgumentException("No statusListIndex from issuerCredentialStore")

val credentialStatus = CredentialStatus(getRevocationListUrlFor(timePeriod), statusListIndex)
val disclosures = credential.claims
.map { SelectiveDisclosureItem(Random.nextBytes(32), it.name, it.value) }
.map { it.toDisclosure() }
val disclosureDigests = disclosures
.map { it.hashDisclosure() }
val (disclosures, disclosureDigests) = credential.toDisclosuresAndDigests()
val cnf = ConfirmationClaim(jsonWebKey = credential.subjectPublicKey.toJsonWebKey())
val jwsPayload = VerifiableCredentialSdJwt(
subject = subjectId,
notBefore = issuanceDate,
Expand All @@ -178,7 +178,7 @@ class IssuerAgent(
disclosureDigests = disclosureDigests,
verifiableCredentialType = credential.scheme.sdJwtType ?: credential.scheme.schemaUri,
selectiveDisclosureAlgorithm = "sha-256",
confirmationKey = credential.subjectPublicKey.toJsonWebKey(),
cnfElement = vckJsonSerializer.encodeToJsonElement(cnf),
credentialStatus = credentialStatus,
).serialize().encodeToByteArray()
val jws = jwsService.createSignedJwt(JwsContentTypeConstants.SD_JWT, jwsPayload).getOrElse {
Expand All @@ -189,6 +189,21 @@ class IssuerAgent(
return Issuer.IssuedCredential.VcSdJwt(vcInSdJwt, credential.scheme)
}

data class DisclosuresAndDigests(
val disclosures: Collection<String>,
val digests: Collection<String>,
)

private fun CredentialToBeIssued.VcSd.toDisclosuresAndDigests(): DisclosuresAndDigests {
val disclosures = claims
.map { SelectiveDisclosureItem(Random.nextBytes(32), it.name, it.value) }
JesusMcCloud marked this conversation as resolved.
Show resolved Hide resolved
.map { it.toDisclosure() }
// may also include decoy digests
val disclosureDigests = disclosures
.map { it.hashDisclosure() }
return DisclosuresAndDigests(disclosures, disclosureDigests)
}

/**
* Wraps the revocation information from [issuerCredentialStore] into a VC,
* returns a JWS representation of that.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,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.VerifiableCredential
import at.asitplus.wallet.lib.data.VerifiableCredentialJws
import at.asitplus.wallet.lib.data.VerifiableCredentialSdJwt
import at.asitplus.wallet.lib.data.*
import at.asitplus.wallet.lib.iso.IssuerSigned
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
Expand Down Expand Up @@ -33,7 +29,7 @@ interface SubjectCredentialStore {
* Passed credentials have been validated before.
*
* @param vc Instance of [VerifiableCredentialSdJwt]
* @param vcSerialized Serialized form of [VerifiableCredential]
* @param vcSerialized Serialized form of [at.asitplus.wallet.lib.jws.SdJwtSigned]
*/
suspend fun storeCredential(
vc: VerifiableCredentialSdJwt,
Expand Down Expand Up @@ -80,9 +76,7 @@ interface SubjectCredentialStore {
val vcSerialized: String,
@SerialName("sd-jwt")
val sdJwt: VerifiableCredentialSdJwt,
/**
* Map of original serialized disclosure item to parsed item
*/
/** Map of serialized disclosure item (as [String]) to parsed item (as [SelectiveDisclosureItem]) */
@SerialName("disclosures")
val disclosures: Map<String, SelectiveDisclosureItem?>,
@SerialName("scheme")
Expand Down
Loading
Loading