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

Credential Representations #25

Merged
merged 13 commits into from
Oct 30, 2023
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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
# Changelog

Release 3.2.0
- Support representing credentials in all three representations: Plain JWT, SD-JWT and ISO MDOC
- Remove property `credentialFormat` from interface `CredentialScheme`, also enum `CredentialFormat`
- Remove property `credentialDefinitionName` from interface `CredentialScheme`, is now automatically converted from `vcType`
- Add properties `isoNamespace` and `isoDocType` to interface `CredentialScheme`, to be used for representing custom credentials according to ISO 18013-5
- Remove function `storeValidatedCredentials` from interface `Holder` and its implementation `HolderAgent`
- Remove class `Holder.ValidatedVerifiableCredentialJws`
- Add member for `CredentialScheme` to various classes like `CredentialToBeIssued.Vc`, subclasses of `IssuedCredential`, subclasses of `StoreCredentialInput` and subclasses of `StoreEntry`
- Add parameter for `CredentialScheme` to methods in `SubjectCredentialStore`
- Remove function `getClaims()` from `CredentialSubject`, logic moved to `IssuerCredentialDataProvider`
- Add parameter `representation` to method `getCredentialWithType` in interface `IssuerCredentialDataProvider`
- Add function `storeGetNextIndex(String, String, Instant, Instant, Int)` to interface `IssuerCredentialStore`
- Remove function `issueCredentialWithTypes(String, CryptoPublicKey?, Collection<String>, CredentialRepresentation)` from interface `Issuer` and its implementation `IssuerAgent`
- 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
- Add constructor property `representation` to `OidcSiopVerifier` to select the representation of credentials
- Add constructor property `credentialRepresentation` to `WalletService` (OpenId4VerifiableCredentialIssuance) to select the representation 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
- Rename class `Issuer.IssuedCredential.Vc` to `Issuer.IssuedCredential.VcJwt`
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,10 @@ class YourCredential : at.asitplus.wallet.lib.data.CredentialSubject {
at.asitplus.wallet.lib.LibraryInitializer.registerExtensionLibrary(
at.asitplus.wallet.lib.LibraryInitializer.ExtensionLibraryInfo(
credentialScheme = object : at.asitplus.wallet.lib.data.ConstantIndex.CredentialScheme {
override val credentialDefinitionName: String = "your-credential"
override val schemaUri: String = "https://example.com/schemas/1.0.0/yourcredential.json"
override val vcType: String = "YourCredential2023"
override val credentialFormat: at.asitplus.wallet.lib.data.ConstantIndex.CredentialFormat = at.asitplus.wallet.lib.data.ConstantIndex.CredentialFormat.W3C_VC
override val isoNamespace: String = "com.example.your-credential"
override val isoDocType: String = "com.example.your-credential.iso"
},
serializersModule = kotlinx.serialization.modules.SerializersModule {
kotlinx.serialization.modules.polymorphic(CredentialSubject::class) {
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ kotlin.experimental.tryK2=false
# workaround dokka bug (need to wait for next snapshot build)
org.jetbrains.dokka.classpath.excludePlatformDependencyFiles=true

artifactVersion = 3.1.0-SNAPSHOT
artifactVersion = 3.2.0-SNAPSHOT
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package at.asitplus.wallet.lib.aries

import at.asitplus.wallet.lib.CryptoPublicKey
import at.asitplus.wallet.lib.DataSourceProblem
import at.asitplus.wallet.lib.agent.Holder
import at.asitplus.wallet.lib.agent.Issuer
import at.asitplus.wallet.lib.data.AriesGoalCodeParser
import at.asitplus.wallet.lib.data.AttributeIndex
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.SchemaIndex
Expand Down Expand Up @@ -146,7 +148,7 @@ class IssueCredentialProtocol(
body = OutOfBandInvitationBody(
handshakeProtocols = arrayOf(SchemaIndex.PROT_ISSUE_CRED),
acceptTypes = arrayOf("application/didcomm-signed+json"),
goalCode = "issue-vc-${credentialScheme.credentialDefinitionName}",
goalCode = "issue-vc-${AriesGoalCodeParser.getAriesName(credentialScheme)}",
services = arrayOf(
OutOfBandService(
type = "did-communication",
Expand All @@ -170,7 +172,7 @@ class IssueCredentialProtocol(
}

private fun createRequestCredential(invitation: OutOfBandInvitation, senderKey: JsonWebKey): InternalNextMessage {
val credentialScheme = ConstantIndex.Parser.parseGoalCode(invitation.body.goalCode)
val credentialScheme = AriesGoalCodeParser.parseGoalCode(invitation.body.goalCode)
?: return problemReporter.problemLastMessage(invitation.threadId, "goal-code-unknown")
val message = buildRequestCredentialMessage(credentialScheme, invitation.id)
?: return InternalNextMessage.IncorrectState("holder")
Expand All @@ -192,7 +194,7 @@ class IssueCredentialProtocol(
issuer = "somebody",
subject = subject,
credential = CredentialDefinition(
name = credentialScheme.credentialDefinitionName,
name = credentialScheme.vcType,
schema = SchemaReference(uri = credentialScheme.schemaUri),
)
)
Expand All @@ -203,7 +205,7 @@ class IssueCredentialProtocol(
return RequestCredential(
body = RequestCredentialBody(
comment = "Please issue some credentials",
goalCode = "issue-vc-${credentialScheme.credentialDefinitionName}",
goalCode = "issue-vc-${AriesGoalCodeParser.getAriesName(credentialScheme)}",
formats = arrayOf(
AttachmentFormatReference(
attachmentId = attachment.id,
Expand All @@ -222,19 +224,22 @@ class IssueCredentialProtocol(
val requestCredentialAttachment = lastJwmAttachment.decodeString()?.let {
RequestCredentialAttachment.deserialize(it)
} ?: return problemReporter.problemLastMessage(lastMessage.threadId, "attachments-format")
val uri = requestCredentialAttachment.credentialManifest.credential.schema.uri

//default pupilID use case: binding key outside JWM, no subject in JWM. set subject to override
val subjectIdentifier = requestCredentialAttachment.credentialManifest.subject ?: senderKey.identifier

val requestedAttributeType = AttributeIndex.getTypeOfAttributeForSchemaUri(uri)
val uri = requestCredentialAttachment.credentialManifest.credential.schema.uri
val requestedCredentialScheme = AttributeIndex.resolveSchemaUri(uri)
val requestedAttributeType = requestedCredentialScheme?.vcType
?: return problemReporter.problemLastMessage(lastMessage.threadId, "requested-attributes-empty")

// TODO Is there a way to transport the format, i.e. JWT-VC or SD-JWT?
requestCredentialAttachment.credentialManifest.credential
val issuedCredentials =
issuer?.issueCredentialWithTypes(subjectIdentifier, attributeTypes = listOf(requestedAttributeType))
?: return problemReporter.problemInternal(lastMessage.threadId, "credentials-empty")
val cryptoPublicKey =
requestCredentialAttachment.credentialManifest.subject?.let { CryptoPublicKey.Ec.fromKeyId(it) }
?: senderKey.toCryptoPublicKey()
?: return problemReporter.problemInternal(lastMessage.threadId, "no-sender-key")
val issuedCredentials = issuer?.issueCredential(
subjectPublicKey = cryptoPublicKey,
attributeTypes = listOf(requestedAttributeType),
representation = ConstantIndex.CredentialRepresentation.PLAIN_JWT
) ?: return problemReporter.problemInternal(lastMessage.threadId, "credentials-empty")

//TODO: Pack this info into `args` or `comment`
if (issuedCredentials.failed.isNotEmpty()) {
Expand All @@ -251,22 +256,30 @@ class IssueCredentialProtocol(

val fulfillmentAttachments = mutableListOf<JwmAttachment>()
val binaryAttachments = mutableListOf<JwmAttachment>()
issuedCredentials.successful.filterIsInstance<Issuer.IssuedCredential.VcJwt>().forEach { cred ->
val fulfillment = JwmAttachment.encodeJws(cred.vcJws)
val binary = cred.attachments?.map {
JwmAttachment.encode(
data = it.data,
filename = it.name,
mediaType = it.mediaType,
parent = fulfillment.id
)
} ?: listOf()
fulfillmentAttachments.add(fulfillment)
binaryAttachments.addAll(binary)
}
issuedCredentials.successful.filterIsInstance<Issuer.IssuedCredential.Iso>().forEach { cred ->
val fulfillment = JwmAttachment.encodeBase64(cred.issuerSigned.serialize())
fulfillmentAttachments.add(fulfillment)
issuedCredentials.successful.forEach { cred ->
when (cred) {
is Issuer.IssuedCredential.Iso -> {
fulfillmentAttachments.add(JwmAttachment.encodeBase64(cred.issuerSigned.serialize()))
}

is Issuer.IssuedCredential.VcJwt -> {
val fulfillment = JwmAttachment.encodeJws(cred.vcJws)
val binary = cred.attachments?.map {
JwmAttachment.encode(
data = it.data,
filename = it.name,
mediaType = it.mediaType,
parent = fulfillment.id
)
} ?: listOf()
fulfillmentAttachments.add(fulfillment)
binaryAttachments.addAll(binary)
}

is Issuer.IssuedCredential.VcSdJwt -> {
fulfillmentAttachments.add(JwmAttachment.encodeJws(cred.vcSdJwt))
}
}
}
val message = IssueCredential(
body = IssueCredentialBody(
Expand Down Expand Up @@ -315,10 +328,10 @@ class IssueCredentialProtocol(
val attachmentList = binaryAttachments
.filter { it.parent == fulfillment.id }
.mapNotNull { extractBinaryAttachment(it) }
return Holder.StoreCredentialInput.Vc(decoded, attachmentList)
return Holder.StoreCredentialInput.Vc(decoded, credentialScheme, attachmentList)
} ?: runCatching { fulfillment.decodeBinary() }.getOrNull()?.let { decoded ->
IssuerSigned.deserialize(decoded)?.let { issuerSigned ->
return Holder.StoreCredentialInput.Iso(issuerSigned)
return Holder.StoreCredentialInput.Iso(issuerSigned, credentialScheme)
}
} ?: return null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class PresentProofMessenger private constructor(
private val serviceEndpoint: String? = null,
private val challengeForPresentation: String = uuid4().toString(),
createProtocolWhenNotActive: Boolean = true,
private val requestedAttributeTypes: Collection<String>? = null,
private val requestedClaims: Collection<String>? = null,
private val credentialScheme: ConstantIndex.CredentialScheme,
) : ProtocolMessenger<PresentProofProtocol, PresentProofProtocolResult>(
messageWrapper = messageWrapper,
Expand All @@ -26,7 +26,7 @@ class PresentProofMessenger private constructor(
override fun createProtocolInstance() = PresentProofProtocol(
verifier = verifier,
holder = holder,
requestedAttributeTypes = requestedAttributeTypes,
requestedClaims = requestedClaims,
credentialScheme = credentialScheme,
serviceEndpoint = serviceEndpoint,
challengeForPresentation = challengeForPresentation,
Expand Down Expand Up @@ -57,12 +57,12 @@ class PresentProofMessenger private constructor(
verifier: Verifier,
messageWrapper: MessageWrapper,
credentialScheme: ConstantIndex.CredentialScheme,
requestedAttributeTypes: Collection<String>? = null,
requestedClaims: Collection<String>? = null,
challengeForPresentation: String = uuid4().toString()
) = PresentProofMessenger(
verifier = verifier,
messageWrapper = messageWrapper,
requestedAttributeTypes = requestedAttributeTypes,
requestedClaims = requestedClaims,
credentialScheme = credentialScheme,
challengeForPresentation = challengeForPresentation,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package at.asitplus.wallet.lib.aries

import at.asitplus.wallet.lib.agent.Holder
import at.asitplus.wallet.lib.agent.Verifier
import at.asitplus.wallet.lib.data.AriesGoalCodeParser
import at.asitplus.wallet.lib.data.ConstantIndex
import at.asitplus.wallet.lib.data.SchemaIndex
import at.asitplus.wallet.lib.data.dif.Constraint
Expand Down Expand Up @@ -49,7 +50,7 @@ typealias PresentProofProtocolResult = Verifier.VerifyPresentationResult
class PresentProofProtocol(
private val holder: Holder? = null,
private val verifier: Verifier? = null,
private val requestedAttributeTypes: Collection<String>? = null,
private val requestedClaims: Collection<String>? = null,
private val credentialScheme: ConstantIndex.CredentialScheme,
private val serviceEndpoint: String?,
private val challengeForPresentation: String,
Expand Down Expand Up @@ -79,10 +80,10 @@ class PresentProofProtocol(
verifier: Verifier,
serviceEndpoint: String? = null,
credentialScheme: ConstantIndex.CredentialScheme,
requestedAttributeTypes: Collection<String>? = null,
requestedClaims: Collection<String>? = null,
) = PresentProofProtocol(
verifier = verifier,
requestedAttributeTypes = requestedAttributeTypes,
requestedClaims = requestedClaims,
credentialScheme = credentialScheme,
serviceEndpoint = serviceEndpoint,
challengeForPresentation = uuid4().toString()
Expand Down Expand Up @@ -161,7 +162,7 @@ class PresentProofProtocol(
body = OutOfBandInvitationBody(
handshakeProtocols = arrayOf(SchemaIndex.PROT_PRESENT_PROOF),
acceptTypes = arrayOf("application/didcomm-encrypted+json"),
goalCode = "request-proof-${credentialScheme.credentialDefinitionName}",
goalCode = "request-proof-${AriesGoalCodeParser.getAriesName(credentialScheme)}",
services = arrayOf(
OutOfBandService(
type = "did-communication",
Expand All @@ -185,7 +186,7 @@ class PresentProofProtocol(
}

private fun createRequestPresentation(invitation: OutOfBandInvitation, senderKey: JsonWebKey): InternalNextMessage {
val credentialScheme = ConstantIndex.Parser.parseGoalCode(invitation.body.goalCode)
val credentialScheme = AriesGoalCodeParser.parseGoalCode(invitation.body.goalCode)
?: return problemReporter.problemLastMessage(invitation.threadId, "goal-code-unknown")
val message = buildRequestPresentationMessage(credentialScheme, invitation.id)
?: return InternalNextMessage.IncorrectState("verifier")
Expand All @@ -203,15 +204,15 @@ class PresentProofProtocol(
): RequestPresentation? {
val verifierIdentifier = verifier?.identifier
?: return null
val constraintsExtraTypes = requestedAttributeTypes?.map(this::buildConstraintFieldForType) ?: listOf()
val constraintsTypes = buildConstraintFieldForType(credentialScheme.vcType)
val claimsConstraints = requestedClaims?.map(this::buildConstraintFieldForClaim) ?: listOf()
val typeConstraints = buildConstraintFieldForType(credentialScheme.vcType)
val presentationDefinition = PresentationDefinition(
inputDescriptors = arrayOf(
InputDescriptor(
name = credentialScheme.credentialDefinitionName,
name = credentialScheme.vcType,
schema = SchemaReference(uri = credentialScheme.schemaUri),
constraints = Constraint(
fields = (constraintsExtraTypes + constraintsTypes).toTypedArray()
fields = (claimsConstraints + typeConstraints).toTypedArray()
)
)
),
Expand Down Expand Up @@ -247,6 +248,11 @@ class PresentProofProtocol(
filter = ConstraintFilter(type = "string", const = attributeType)
)

private fun buildConstraintFieldForClaim(claimName: String) = ConstraintField(
path = arrayOf("\$.vc[*].name", "\$.type"),
filter = ConstraintFilter(type = "string", const = claimName)
)

private suspend fun createPresentation(
lastMessage: RequestPresentation,
senderKey: JsonWebKey
Expand All @@ -261,17 +267,24 @@ class PresentProofProtocol(
RequestPresentationAttachment.deserialize(it)
} ?: return problemReporter.problemLastMessage(lastMessage.threadId, "attachments-format")
// TODO Is ISO supported here?
val requestedTypes = requestPresentationAttachment.presentationDefinition.inputDescriptors
val constraintFields = requestPresentationAttachment.presentationDefinition.inputDescriptors
.mapNotNull { it.constraints }
.flatMap { it.fields?.toList() ?: listOf() }
val requestedTypes = constraintFields
.filter { it.path.contains("\$.vc[*].type") }
.mapNotNull { it.filter }
.filter { it.type == "string" }
.mapNotNull { it.const }
val requestedClaims = constraintFields
.filter { it.path.contains("\$.vc[*].name") }
.mapNotNull { it.filter }
.filter { it.type == "string" }
.mapNotNull { it.const }
val vp = holder?.createPresentation(
requestPresentationAttachment.options.challenge,
requestPresentationAttachment.options.verifier ?: senderKey.identifier,
challenge = requestPresentationAttachment.options.challenge,
audienceId = requestPresentationAttachment.options.verifier ?: senderKey.identifier,
attributeTypes = requestedTypes.ifEmpty { null },
requestedClaims = requestedClaims.ifEmpty { null },
) ?: return problemReporter.problemInternal(lastMessage.threadId, "vp-empty")
// TODO is ISO supported here?
if (vp !is Holder.CreatePresentationResult.Signed) {
Expand Down
Loading