From fce300eccf4940b814ad026c8236fdc8d2dc081f Mon Sep 17 00:00:00 2001 From: Cristian Gonzalez <113917899+cristianIOHK@users.noreply.github.com> Date: Thu, 19 Sep 2024 09:25:03 -0400 Subject: [PATCH] feat: connectionless credential offer (#207) --- .../identus/walletsdk/edgeagent/EdgeAgent.kt | 265 ++++++++++++------ .../models/ConnectionlessMessageData.kt | 11 + .../issueCredential/CredentialPreview.kt | 2 +- .../ConnectionlessCredentialOffer.kt | 13 + .../ConnectionlessRequestPresentation.kt | 9 + .../protocols/pickup/PickupRunner.kt | 7 +- .../walletsdk/edgeagent/EdgeAgentTests.kt | 82 +++++- .../ui/contacts/ContactsViewModel.kt | 18 ++ 8 files changed, 309 insertions(+), 98 deletions(-) create mode 100644 edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/models/ConnectionlessMessageData.kt create mode 100644 edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/outOfBand/ConnectionlessCredentialOffer.kt create mode 100644 edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/outOfBand/ConnectionlessRequestPresentation.kt diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgent.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgent.kt index aa57cb30a..27bc02fd9 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgent.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgent.kt @@ -60,8 +60,9 @@ import org.hyperledger.identus.walletsdk.domain.buildingblocks.Pollux import org.hyperledger.identus.walletsdk.domain.models.Api import org.hyperledger.identus.walletsdk.domain.models.ApiImpl import org.hyperledger.identus.walletsdk.domain.models.ApolloError +import org.hyperledger.identus.walletsdk.domain.models.AttachmentData import org.hyperledger.identus.walletsdk.domain.models.AttachmentData.AttachmentBase64 -import org.hyperledger.identus.walletsdk.domain.models.AttachmentData.AttachmentJsonData + import org.hyperledger.identus.walletsdk.domain.models.AttachmentDescriptor import org.hyperledger.identus.walletsdk.domain.models.Credential import org.hyperledger.identus.walletsdk.domain.models.CredentialOperationsOptions @@ -93,12 +94,15 @@ import org.hyperledger.identus.walletsdk.domain.models.keyManagement.TypeKey import org.hyperledger.identus.walletsdk.edgeagent.helpers.AgentOptions import org.hyperledger.identus.walletsdk.edgeagent.mediation.BasicMediatorHandler import org.hyperledger.identus.walletsdk.edgeagent.mediation.MediationHandler +import org.hyperledger.identus.walletsdk.edgeagent.models.ConnectionlessMessageData import org.hyperledger.identus.walletsdk.edgeagent.protocols.ProtocolType import org.hyperledger.identus.walletsdk.edgeagent.protocols.connection.DIDCommConnectionRunner import org.hyperledger.identus.walletsdk.edgeagent.protocols.findProtocolTypeByValue import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.IssueCredential import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.OfferCredential import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.RequestCredential +import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.ConnectionlessCredentialOffer +import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.ConnectionlessRequestPresentation import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.DIDCommInvitationRunner import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.InvitationType import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.OutOfBandInvitation @@ -818,7 +822,12 @@ open class EdgeAgent { @Throws(EdgeAgentError.UnknownInvitationTypeError::class, SerializationException::class) suspend fun parseInvitation(str: String): InvitationType { Url.parse(str)?.let { - return parseOOBInvitation(it) + val outOfBandInvitation = parseOOBInvitation(it) + if (outOfBandInvitation.attachments.isNotEmpty()) { + return connectionlessInvitation(outOfBandInvitation) + } else { + return outOfBandInvitation + } } ?: run { try { val json = Json.decodeFromString(str) @@ -898,7 +907,7 @@ open class EdgeAgent { * @return The parsed Out-of-Band invitation * @throws [EdgeAgentError.UnknownInvitationTypeError] if the URL is not a valid Out-of-Band invitation */ - private suspend fun parseOOBInvitation(url: Url): OutOfBandInvitation { + private fun parseOOBInvitation(url: Url): OutOfBandInvitation { return DIDCommInvitationRunner(url).run() } @@ -910,84 +919,9 @@ open class EdgeAgent { */ suspend fun acceptOutOfBandInvitation(invitation: OutOfBandInvitation) { val ownDID = createNewPeerDID(updateMediator = true) - if (invitation.attachments.isNotEmpty()) { - // If attachments not empty, means connectionless presentation - val now = Instant.fromEpochMilliseconds(getTimeMillis()) - val expiryDate = Instant.fromEpochSeconds(invitation.expiresTime) - if (now > expiryDate) { - throw EdgeAgentError.ExpiredInvitation() - } - - val jsonString = invitation.attachments.firstNotNullOf { it.data.getDataAsJsonString() } - val requestPresentationJson = Json.parseToJsonElement(jsonString).jsonObject - if (!requestPresentationJson.containsKey("id")) { - throw EdgeAgentError.MissingOrNullFieldError("id", "Request") - } - if (!requestPresentationJson.containsKey("body")) { - throw EdgeAgentError.MissingOrNullFieldError("body", "Request") - } - if (!requestPresentationJson.containsKey("attachments")) { - throw EdgeAgentError.MissingOrNullFieldError("attachments", "Request") - } - if (!requestPresentationJson.containsKey("thid")) { - throw EdgeAgentError.MissingOrNullFieldError("thid", "Request") - } - if (!requestPresentationJson.containsKey("from")) { - throw EdgeAgentError.MissingOrNullFieldError("from", "Request") - } - - val requestId = requestPresentationJson["id"]!! - val requestBody = requestPresentationJson["body"]!! - val requestAttachments = requestPresentationJson["attachments"]!! - val requestThid = requestPresentationJson["thid"]!! - val requestFrom = requestPresentationJson["from"]!! - - if (requestAttachments.jsonArray.size == 0) { - throw EdgeAgentError.MissingOrNullFieldError("attachments", "Request") - } - val attachmentJsonObject = requestAttachments.jsonArray[0] - if (!attachmentJsonObject.jsonObject.containsKey("id")) { - throw EdgeAgentError.MissingOrNullFieldError("id", "Request attachments") - } - if (!attachmentJsonObject.jsonObject.containsKey("media_type")) { - throw EdgeAgentError.MissingOrNullFieldError("media_type", "Request attachments") - } - if (!attachmentJsonObject.jsonObject.containsKey("data")) { - if (!attachmentJsonObject.jsonObject["data"]!!.jsonObject.containsKey("json")) { - throw EdgeAgentError.MissingOrNullFieldError("json", "Request attachments data") - } - throw EdgeAgentError.MissingOrNullFieldError("data", "Request attachments") - } - if (!attachmentJsonObject.jsonObject.containsKey("format")) { - throw EdgeAgentError.MissingOrNullFieldError("format", "Request attachments") - } - val attachmentId = attachmentJsonObject.jsonObject["id"]!! - val attachmentMediaType = attachmentJsonObject.jsonObject["media_type"]!! - val attachmentData = attachmentJsonObject.jsonObject["data"]!!.jsonObject["json"]!! - val attachmentFormat = attachmentJsonObject.jsonObject["format"]!! - - val attachmentDescriptor = AttachmentDescriptor( - id = attachmentId.jsonPrimitive.content, - mediaType = attachmentMediaType.jsonPrimitive.content, - data = AttachmentJsonData(attachmentData.toString()), - format = attachmentFormat.jsonPrimitive.content - ) - - val requestPresentation = RequestPresentation( - id = requestId.jsonPrimitive.content, - body = Json.decodeFromString(requestBody.jsonObject.toString()), - attachments = arrayOf(attachmentDescriptor), - thid = requestThid.jsonPrimitive.content, - from = DID(requestFrom.jsonPrimitive.content), - to = ownDID - ) - - pluto.storeMessage(requestPresentation.makeMessage()) - } else { - // Regular OOB invitation - val pair = DIDCommConnectionRunner(invitation, pluto, ownDID, connectionManager).run() - connectionManager.addConnection(pair) - } + // Regular OOB invitation + val pair = DIDCommConnectionRunner(invitation, pluto, ownDID, connectionManager).run() + connectionManager.addConnection(pair) } /** @@ -1492,6 +1426,175 @@ open class EdgeAgent { return nonce.toString() } + /** + * Parses and validates a connectionless message from a JSON object. The method checks for the existence + * of required fields (e.g., id, body, attachments, thid, from) and throws errors if any are missing. + * It extracts necessary information from the message, including the attachment details, and returns + * a ConnectionlessMessageData object containing the parsed information. + * + * @param messageJson The JsonObject representing the connectionless message. + * @return A ConnectionlessMessageData object containing the parsed message data. + * @throws EdgeAgentError.MissingOrNullFieldError if any required field is missing or null. + */ + private fun parseAndValidateMessage(messageJson: JsonObject): ConnectionlessMessageData { + // Perform validation + if (!messageJson.containsKey("id")) throw EdgeAgentError.MissingOrNullFieldError("id", "Request") + if (!messageJson.containsKey("body")) throw EdgeAgentError.MissingOrNullFieldError("body", "Request") + if (!messageJson.containsKey("attachments")) { + throw EdgeAgentError.MissingOrNullFieldError( + "attachments", + "Request" + ) + } + if (!messageJson.containsKey("thid")) throw EdgeAgentError.MissingOrNullFieldError("thid", "Request") + if (!messageJson.containsKey("from")) throw EdgeAgentError.MissingOrNullFieldError("from", "Request") + + val messageId = messageJson["id"]!!.jsonPrimitive.content + val messageBody = messageJson["body"]!!.toString() + val messageThid = messageJson["thid"]!!.jsonPrimitive.content + val messageFrom = messageJson["from"]!!.jsonPrimitive.content + + // Validate and parse the first attachment + val attachmentJsonObject = messageJson["attachments"]!!.jsonArray.first().jsonObject + if (!attachmentJsonObject.containsKey("id")) { + throw EdgeAgentError.MissingOrNullFieldError( + "id", + "Request attachments" + ) + } + if (!attachmentJsonObject.containsKey("media_type")) { + throw EdgeAgentError.MissingOrNullFieldError( + "media_type", + "Request attachments" + ) + } + if (!attachmentJsonObject.containsKey("data")) { + throw EdgeAgentError.MissingOrNullFieldError( + "data", + "Request attachments" + ) + } + if (!attachmentJsonObject.containsKey("format")) { + throw EdgeAgentError.MissingOrNullFieldError( + "format", + "Request attachments" + ) + } + + val attachmentId = attachmentJsonObject["id"]!!.jsonPrimitive.content + val attachmentMediaType = attachmentJsonObject["media_type"]!!.jsonPrimitive.content + val attachmentData = attachmentJsonObject["data"]!!.jsonObject["json"]!!.toString() + val attachmentFormat = attachmentJsonObject["format"]!!.jsonPrimitive.content + + val attachmentDescriptor = AttachmentDescriptor( + id = attachmentId, + mediaType = attachmentMediaType, + data = AttachmentData.AttachmentJsonData(attachmentData), + format = attachmentFormat + ) + + // Return the extracted data + return ConnectionlessMessageData( + messageId = messageId, + messageBody = messageBody, + attachmentDescriptor = attachmentDescriptor, + messageThid = messageThid, + messageFrom = messageFrom + ) + } + + /** + * Handles a connectionless invitation by parsing the invitation string, extracting the necessary + * message data, and invoking the appropriate handler based on the type of the message. + * + * @param did The DID (Decentralized Identifier) associated with the invitation. + * @param invitationString The JSON string representing the invitation. + * @throws EdgeAgentError.MissingOrNullFieldError if any required field is missing or null. + * @throws EdgeAgentError.UnknownInvitationTypeError if the invitation type is unknown. + */ + private suspend fun connectionlessInvitation(outOfBandInvitation: OutOfBandInvitation): InvitationType { + val now = Instant.fromEpochMilliseconds(getTimeMillis()) + val expiryDate = Instant.fromEpochSeconds(outOfBandInvitation.expiresTime) + if (now > expiryDate) { + throw EdgeAgentError.ExpiredInvitation() + } + + val jsonString = outOfBandInvitation.attachments.firstNotNullOf { it.data.getDataAsJsonString() } + val invitation = Json.parseToJsonElement(jsonString) + if (invitation.jsonObject.containsKey("type")) { + val type = invitation.jsonObject["type"]?.jsonPrimitive?.content + val attachments = invitation.jsonObject["attachments"]!!.jsonArray + if (attachments.isEmpty()) { + throw Exception() + } + val connectionLessMessageData = parseAndValidateMessage(invitation.jsonObject) + return when (type) { + ProtocolType.DidcommOfferCredential.value -> { + handleConnectionlessOfferCredential(connectionLessMessageData) + } + + ProtocolType.DidcommRequestPresentation.value -> { + handleConnectionlessRequestPresentation(connectionLessMessageData) + } + + else -> { + throw EdgeAgentError.UnknownInvitationTypeError(type ?: "null") + } + } + } else { + throw EdgeAgentError.MissingOrNullFieldError("type", "connectionless invitation") + } + } + + /** + * Handles a connectionless Offer Credential message by extracting the necessary data + * from the ConnectionlessMessageData and storing the message using the Pluto service. + * + * @param connectionlessMessageData The parsed data from the connectionless message. + */ + private suspend fun handleConnectionlessOfferCredential( + connectionlessMessageData: ConnectionlessMessageData + ): InvitationType { + val did = createNewPeerDID(updateMediator = true) + val offerCredential = OfferCredential( + id = connectionlessMessageData.messageId, + body = Json.decodeFromString(connectionlessMessageData.messageBody), + attachments = arrayOf(connectionlessMessageData.attachmentDescriptor), + thid = connectionlessMessageData.messageThid, + from = DID(connectionlessMessageData.messageFrom), + to = did + ) + pluto.storeMessage(offerCredential.makeMessage()) + return ConnectionlessCredentialOffer( + offerCredential = offerCredential + ) + } + + /** + * Handles a connectionless Request Presentation message by extracting the necessary data + * from the ConnectionlessMessageData and storing the message using the Pluto service. + * + * @param connectionlessMessageData The parsed data from the connectionless message. + */ + private suspend fun handleConnectionlessRequestPresentation( + connectionlessMessageData: ConnectionlessMessageData + ): InvitationType { + val did = createNewPeerDID(updateMediator = true) + val requestPresentation = RequestPresentation( + id = connectionlessMessageData.messageId, + body = Json.decodeFromString(connectionlessMessageData.messageBody), + attachments = arrayOf(connectionlessMessageData.attachmentDescriptor), + thid = connectionlessMessageData.messageThid, + from = DID(connectionlessMessageData.messageFrom), + to = did + ) + +// pluto.storeMessage(requestPresentation.makeMessage()) + return ConnectionlessRequestPresentation( + requestPresentation = requestPresentation + ) + } + /** * Enumeration representing the current state of the agent. */ diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/models/ConnectionlessMessageData.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/models/ConnectionlessMessageData.kt new file mode 100644 index 000000000..05db6cb21 --- /dev/null +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/models/ConnectionlessMessageData.kt @@ -0,0 +1,11 @@ +package org.hyperledger.identus.walletsdk.edgeagent.models + +import org.hyperledger.identus.walletsdk.domain.models.AttachmentDescriptor + +data class ConnectionlessMessageData( + val messageId: String, + val messageBody: String, + val attachmentDescriptor: AttachmentDescriptor, + val messageThid: String, + val messageFrom: String +) diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/issueCredential/CredentialPreview.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/issueCredential/CredentialPreview.kt index 86b841d95..2c6244bc4 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/issueCredential/CredentialPreview.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/issueCredential/CredentialPreview.kt @@ -106,6 +106,6 @@ constructor( val name: String, val value: String, @SerialName("media_type") - val mediaType: String? + val mediaType: String? = null ) } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/outOfBand/ConnectionlessCredentialOffer.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/outOfBand/ConnectionlessCredentialOffer.kt new file mode 100644 index 000000000..fce59dfbd --- /dev/null +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/outOfBand/ConnectionlessCredentialOffer.kt @@ -0,0 +1,13 @@ +package org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand + +import kotlinx.serialization.Serializable +import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.OfferCredential +import java.util.* + +/** + * Represents a connectionless credential offer. + */ +@Serializable +data class ConnectionlessCredentialOffer( + val offerCredential: OfferCredential +) : InvitationType() diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/outOfBand/ConnectionlessRequestPresentation.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/outOfBand/ConnectionlessRequestPresentation.kt new file mode 100644 index 000000000..b50e51634 --- /dev/null +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/outOfBand/ConnectionlessRequestPresentation.kt @@ -0,0 +1,9 @@ +package org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand + +import kotlinx.serialization.Serializable +import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.RequestPresentation + +@Serializable +data class ConnectionlessRequestPresentation( + val requestPresentation: RequestPresentation +) : InvitationType() diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/pickup/PickupRunner.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/pickup/PickupRunner.kt index 16ea76157..1e642130f 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/pickup/PickupRunner.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/pickup/PickupRunner.kt @@ -24,7 +24,8 @@ class PickupRunner(message: Message, private val mercury: Mercury) { */ enum class PickupResponseType(val type: String) { STATUS("status"), - DELIVERY("delivery") + DELIVERY("delivery"), + PROBLEM_REPORT("problem_report") } /** @@ -60,6 +61,10 @@ class PickupRunner(message: Message, private val mercury: Mercury) { this.message = PickupResponse(PickupResponseType.DELIVERY, message) } + ProtocolType.ProblemReport.value -> { + this.message = PickupResponse(PickupResponseType.PROBLEM_REPORT, message) + } + else -> { throw EdgeAgentError.InvalidMessageType( type = message.piuri, diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgentTests.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgentTests.kt index a11936bd4..f1ce8e82e 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgentTests.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgentTests.kt @@ -15,7 +15,9 @@ import kotlinx.coroutines.test.runTest import kotlinx.serialization.Serializable import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json +import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive import org.hyperledger.identus.apollo.base64.base64UrlDecoded import org.hyperledger.identus.apollo.base64.base64UrlDecodedBytes import org.hyperledger.identus.apollo.base64.base64UrlEncoded @@ -69,6 +71,8 @@ import org.hyperledger.identus.walletsdk.edgeagent.mediation.MediationHandler import org.hyperledger.identus.walletsdk.edgeagent.protocols.ProtocolType import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.IssueCredential import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.OfferCredential +import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.ConnectionlessCredentialOffer +import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.ConnectionlessRequestPresentation import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.OutOfBandInvitation import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.PrismOnboardingInvitation import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.RequestPresentation @@ -1874,13 +1878,8 @@ class EdgeAgentTests { val outOfBandUrl = "https://my.domain.com/path?_oob=eyJpZCI6IjViMjUwMjIzLWExNDItNDRmYi1hOWJkLWU1MjBlNGI0ZjQzMiIsInR5cGUiOiJodHRwczovL2RpZGNvbW0ub3JnL291dC1vZi1iYW5kLzIuMC9pbnZpdGF0aW9uIiwiZnJvbSI6ImRpZDpwZWVyOjIuRXo2TFNkV0hWQ1BFOHc0NWZETjM4aUh0ZFJ6WGkyTFNqQmRSUjRGTmNOUm12VkNKcy5WejZNa2Z2aUI5S1F1OGlnNVZpeG1HZHM3dmdMNmoyUXNOUGFybkZaanBNQ0E5aHpQLlNleUowSWpvaVpHMGlMQ0p6SWpwN0luVnlhU0k2SW1oMGRIQTZMeTh4T1RJdU1UWTRMakV1TXpjNk9EQTNNQzlrYVdSamIyMXRJaXdpY2lJNlcxMHNJbUVpT2xzaVpHbGtZMjl0YlM5Mk1pSmRmWDAiLCJib2R5Ijp7ImdvYWxfY29kZSI6InByZXNlbnQtdnAiLCJnb2FsIjoiUmVxdWVzdCBwcm9vZiBvZiB2YWNjaW5hdGlvbiBpbmZvcm1hdGlvbiIsImFjY2VwdCI6W119LCJhdHRhY2htZW50cyI6W3siaWQiOiIyYTZmOGM4NS05ZGE3LTRkMjQtOGRhNS0wYzliZDY5ZTBiMDEiLCJtZWRpYV90eXBlIjoiYXBwbGljYXRpb24vanNvbiIsImRhdGEiOnsianNvbiI6eyJpZCI6IjI1NTI5MTBiLWI0NmMtNDM3Yy1hNDdhLTlmODQ5OWI5ZTg0ZiIsInR5cGUiOiJodHRwczovL2RpZGNvbW0uYXRhbGFwcmlzbS5pby9wcmVzZW50LXByb29mLzMuMC9yZXF1ZXN0LXByZXNlbnRhdGlvbiIsImJvZHkiOnsiZ29hbF9jb2RlIjoiUmVxdWVzdCBQcm9vZiBQcmVzZW50YXRpb24iLCJ3aWxsX2NvbmZpcm0iOmZhbHNlLCJwcm9vZl90eXBlcyI6W119LCJhdHRhY2htZW50cyI6W3siaWQiOiJiYWJiNTJmMS05NDUyLTQzOGYtYjk3MC0yZDJjOTFmZTAyNGYiLCJtZWRpYV90eXBlIjoiYXBwbGljYXRpb24vanNvbiIsImRhdGEiOnsianNvbiI6eyJvcHRpb25zIjp7ImNoYWxsZW5nZSI6IjExYzkxNDkzLTAxYjMtNGM0ZC1hYzM2LWIzMzZiYWI1YmRkZiIsImRvbWFpbiI6Imh0dHBzOi8vcHJpc20tdmVyaWZpZXIuY29tIn0sInByZXNlbnRhdGlvbl9kZWZpbml0aW9uIjp7ImlkIjoiMGNmMzQ2ZDItYWY1Ny00Y2E1LTg2Y2EtYTA1NTE1NjZlYzZmIiwiaW5wdXRfZGVzY3JpcHRvcnMiOltdfX19LCJmb3JtYXQiOiJwcmlzbS9qd3QifV0sInRoaWQiOiI1YjI1MDIyMy1hMTQyLTQ0ZmItYTliZC1lNTIwZTRiNGY0MzIiLCJmcm9tIjoiZGlkOnBlZXI6Mi5FejZMU2RXSFZDUEU4dzQ1ZkROMzhpSHRkUnpYaTJMU2pCZFJSNEZOY05SbXZWQ0pzLlZ6Nk1rZnZpQjlLUXU4aWc1Vml4bUdkczd2Z0w2ajJRc05QYXJuRlpqcE1DQTloelAuU2V5SjBJam9pWkcwaUxDSnpJanA3SW5WeWFTSTZJbWgwZEhBNkx5OHhPVEl1TVRZNExqRXVNemM2T0RBM01DOWthV1JqYjIxdElpd2ljaUk2VzEwc0ltRWlPbHNpWkdsa1kyOXRiUzkyTWlKZGZYMCJ9fX1dLCJjcmVhdGVkX3RpbWUiOjE3MjQzMzkxNDQsImV4cGlyZXNfdGltZSI6MTcyNDMzOTQ0NH0" - val oob = agent.parseInvitation(outOfBandUrl) - assertTrue(oob is OutOfBandInvitation) - oob as OutOfBandInvitation - - doReturn(DID("did:peer:asdf")).`when`(agent).createNewPeerDID(updateMediator = true) assertFailsWith(EdgeAgentError.ExpiredInvitation::class) { - agent.acceptOutOfBandInvitation(oob) + agent.parseInvitation(outOfBandUrl) } } @@ -1904,18 +1903,13 @@ class EdgeAgentTests { val notExpiredInvitation = """{"id":"5b250223-a142-44fb-a9bd-e520e4b4f432","type":"https://didcomm.org/out-of-band/2.0/invitation","from":"did:peer:2.Ez6LSdWHVCPE8w45fDN38iHtdRzXi2LSjBdRR4FNcNRmvVCJs.Vz6MkfviB9KQu8ig5VixmGds7vgL6j2QsNParnFZjpMCA9hzP.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly8xOTIuMTY4LjEuMzc6ODA3MC9kaWRjb21tIiwiciI6W10sImEiOlsiZGlkY29tbS92MiJdfX0","body":{"goal_code":"present-vp","goal":"Request proof of vaccination information","accept":[]},"attachments":[{"id":"2a6f8c85-9da7-4d24-8da5-0c9bd69e0b01","media_type":"application/json","data":{"json":{"id":"2552910b-b46c-437c-a47a-9f8499b9e84f","type":"https://didcomm.atalaprism.io/present-proof/3.0/request-presentation","body":{"goal_code":"Request Proof Presentation","will_confirm":false,"proof_types":[]},"attachments":[{"id":"babb52f1-9452-438f-b970-2d2c91fe024f","media_type":"application/json","data":{"json":{"options":{"challenge":"11c91493-01b3-4c4d-ac36-b336bab5bddf","domain":"https://prism-verifier.com"},"presentation_definition":{"id":"0cf346d2-af57-4ca5-86ca-a0551566ec6f","input_descriptors":[]}}},"format":"prism/jwt"}],"thid":"5b250223-a142-44fb-a9bd-e520e4b4f432","from":"did:peer:2.Ez6LSdWHVCPE8w45fDN38iHtdRzXi2LSjBdRR4FNcNRmvVCJs.Vz6MkfviB9KQu8ig5VixmGds7vgL6j2QsNParnFZjpMCA9hzP.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly8xOTIuMTY4LjEuMzc6ODA3MC9kaWRjb21tIiwiciI6W10sImEiOlsiZGlkY29tbS92MiJdfX0"}}}],"created_time":1724339144,"expires_time":$notExpiredTime}""" val base64Invitation = notExpiredInvitation.base64UrlEncoded + doReturn(DID("did:peer:asdf")).`when`(agent).createNewPeerDID(updateMediator = true) val outOfBandUrl = "https://my.domain.com/path?_oob=$base64Invitation" - val oob = agent.parseInvitation(outOfBandUrl) - assertTrue(oob is OutOfBandInvitation) - oob as OutOfBandInvitation + val connectionlessRequestPresentation = agent.parseInvitation(outOfBandUrl) + assertTrue(connectionlessRequestPresentation is ConnectionlessRequestPresentation) + val msg = (connectionlessRequestPresentation as ConnectionlessRequestPresentation).requestPresentation - doReturn(DID("did:peer:asdf")).`when`(agent).createNewPeerDID(updateMediator = true) - agent.acceptOutOfBandInvitation(oob) - val captor = argumentCaptor() - verify(plutoMock).storeMessage(captor.capture()) - val msg = captor.lastValue - assertEquals(ProtocolType.DidcommRequestPresentation.value, msg.piuri) assertEquals("5b250223-a142-44fb-a9bd-e520e4b4f432", msg.thid) val attachments = msg.attachments assertEquals(1, attachments.size) @@ -1926,6 +1920,64 @@ class EdgeAgentTests { assertTrue(json.jsonObject.containsKey("presentation_definition")) } + @Test + fun `test connectionless credential offer correctly`() = runTest { + val agent = spy( + EdgeAgent( + apollo = apolloMock, + castor = castorMock, + pluto = plutoMock, + mercury = mercuryMock, + pollux = polluxMock, + connectionManager = connectionManagerMock, + seed = seed, + api = null, + logger = LoggerMock() + ) + ) + + val notExpiredTime = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(30) + val notExpiredInvitation = + """{"id":"f96e3699-591c-4ae7-b5e6-6efe6d26255b","type":"https://didcomm.org/out-of-band/2.0/invitation","from":"did:peer:2.Ez6LSfsKMe8vSSWkYdZCpn4YViPERfdGAhdLAGHgx2LGJwfmA.Vz6Mkpw1kSabBMzkA3v59tQFnh3FtkKy6xLhLxd9S6BAoaBg2.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly8xOTIuMTY4LjEuMzc6ODA4MC9kaWRjb21tIiwiciI6W10sImEiOlsiZGlkY29tbS92MiJdfX0","body":{"goal_code":"issue-vc","goal":"To issue a Faber College Graduate credential","accept":["didcomm/v2"]},"attachments":[{"id":"70cdc90c-9a99-4cda-87fe-4f4b2595112a","media_type":"application/json","data":{"json":{"id":"655e9a2c-48ed-459b-b3da-6b3686655564","type":"https://didcomm.org/issue-credential/3.0/offer-credential","body":{"goal_code":"Offer Credential","credential_preview":{"type":"https://didcomm.org/issue-credential/3.0/credential-credential","body":{"attributes":[{"name":"familyName","value":"Wonderland"},{"name":"givenName","value":"Alice"},{"name":"drivingClass","value":"Mw==","media_type":"application/json"},{"name":"dateOfIssuance","value":"2020-11-13T20:20:39+00:00"},{"name":"emailAddress","value":"alice@wonderland.com"},{"name":"drivingLicenseID","value":"12345"}]}}},"attachments":[{"id":"8404678b-9a36-4989-af1d-0f445347e0e3","media_type":"application/json","data":{"json":{"options":{"challenge":"ad0f43ad-8538-41d4-9cb8-20967bc685bc","domain":"domain"},"presentation_definition":{"id":"748efa58-2bce-440d-921f-2520a8446663","input_descriptors":[],"format":{"jwt":{"alg":["ES256K"],"proof_type":[]}}}}},"format":"prism/jwt"}],"thid":"f96e3699-591c-4ae7-b5e6-6efe6d26255b","from":"did:peer:2.Ez6LSfsKMe8vSSWkYdZCpn4YViPERfdGAhdLAGHgx2LGJwfmA.Vz6Mkpw1kSabBMzkA3v59tQFnh3FtkKy6xLhLxd9S6BAoaBg2.SeyJ0IjoiZG0iLCJzIjp7InVyaSI6Imh0dHA6Ly8xOTIuMTY4LjEuMzc6ODA4MC9kaWRjb21tIiwiciI6W10sImEiOlsiZGlkY29tbS92MiJdfX0"}}}],"created_time":1724851139,"expires_time":$notExpiredTime}""" + val base64Invitation = notExpiredInvitation.base64UrlEncoded + doReturn(DID("did:peer:asdf")).`when`(agent).createNewPeerDID(updateMediator = true) + + val outOfBandUrl = "https://my.domain.com/path?_oob=$base64Invitation" + val connectionlessCredentialOffer = agent.parseInvitation(outOfBandUrl) + assertTrue(connectionlessCredentialOffer is ConnectionlessCredentialOffer) + connectionlessCredentialOffer as ConnectionlessCredentialOffer + val msg = connectionlessCredentialOffer.offerCredential.makeMessage() + + assertEquals(ProtocolType.DidcommOfferCredential.value, msg.piuri) + assertEquals("f96e3699-591c-4ae7-b5e6-6efe6d26255b", msg.thid) + val attachments = msg.attachments + assertEquals(1, attachments.size) + val attachmentJsonData = attachments.first().data + assertTrue(attachmentJsonData is AttachmentData.AttachmentJsonData) + val json = Json.parseToJsonElement(attachmentJsonData.getDataAsJsonString()) + assertTrue(json.jsonObject.containsKey("options")) + assertTrue(json.jsonObject["options"]!!.jsonObject.containsKey("challenge")) + assertTrue(json.jsonObject["options"]!!.jsonObject.containsKey("domain")) + assertTrue(json.jsonObject.containsKey("presentation_definition")) + assertTrue(json.jsonObject["presentation_definition"]!!.jsonObject.containsKey("id")) + assertTrue(json.jsonObject["presentation_definition"]!!.jsonObject.containsKey("input_descriptors")) + assertTrue(json.jsonObject["presentation_definition"]!!.jsonObject.containsKey("format")) + assertTrue(json.jsonObject["presentation_definition"]!!.jsonObject["format"]!!.jsonObject.contains("jwt")) + assertTrue( + json.jsonObject["presentation_definition"]!!.jsonObject["format"]!!.jsonObject["jwt"]!!.jsonObject.contains( + "alg" + ) + ) + val algs = + json.jsonObject["presentation_definition"]!!.jsonObject["format"]!!.jsonObject["jwt"]!!.jsonObject["alg"]!!.jsonArray + assertEquals("ES256K", algs.first().jsonPrimitive.content) + assertTrue( + json.jsonObject["presentation_definition"]!!.jsonObject["format"]!!.jsonObject["jwt"]!!.jsonObject.contains( + "proof_type" + ) + ) + } + val getCredentialDefinitionResponse = "{\"schemaId\":\"http://host.docker.internal:8000/prism-agent/schema-registry/schemas/5e0d5a93-4bfd-3111-a956-5d5bc82f76cc\",\"type\":\"CL\",\"tag\":\"licence\",\"value\":{\"primary\":{\"n\":\"105195159277979097653318357586659371305119697478469834190626350283715795188687389523188659352120689851168860621983864738336838773213022505168653440146374011050277159372491059901432822905781969400722059341786498751125483895348734607382548396665339315322605154516776326303787844694026898270194867398625429469096229269732265502538641116512214652017416624138065704599041020588805936844771273861390913500753293895219370960892829297672575154196820931047049021760519166121287056337193413235473255257349024671869248216238831094979209384406168241010010012567685965827447177652200129684927663161550376084422586141212281146491949\",\"s\":\"85376740935726732134199731472843597191822272986425414914465211197069650618238336366149699822721009443794877925725075553195071288777117865451699414058058985000654277974066307286552934230286237253977472401290858765904161191229985245519871949378628131263513153683765553672655918133136828182050729012388157183851720391379381006921499997765191873729408614024320763554099291141052786589157823043612948619201525441997065264492145372001259366749278235381762443117203343617927241093647322654346302447381494008414208398219626199373278313446814209403507903682881070548386699522575055488393512785511441688197244526708647113340516\",\"r\":{\"dateofissuance\":\"16159515692057558658031632775257139859912833740243870833808276956469677196577164655991169139545328065546186056342530531355718904597216453319851305621683589202769847381737819412615902541110462703838858425423753481085962114120185123089078513531045426316918036549403698066078445947881055316312848598741184161901260446303171175343050250045452903485086185722998336149005743485268486377824763449026501058416292877646187105446333888525480394665310217044483841168928926515929150167890936706159800372381200383816724043496032886366767166850459338411710056171379538841845247931898550165532492578625954615979453881721709564750235\",\"drivingclass\":\"83649701835078373520097916558245060224505938113940626586910000950978790663411517512280043632278010831292224659523658613504637416710001103641231226266903556936380105758523760424939825687213460920436570466066231912959327201876189240504388424799892400351592593406285436824571943165913587899115814843543998396726679289422080229750418336051741708013580146373647528674381958028243228435161765957312248113519708734663989428761879029086059388435772829434952754093999424834120341657211221855300108096057633128467059590470639772605075954658131680801785637700237403873940041665483384938586320674338994185073499523485570537331062\",\"emailaddress\":\"96995643129591814391344614133120459563648002327749700279517548454036811217735867585059116635583558148259032071807493674533230465312311981127622542797279917256478867847832932893748528200469349058284133058865149153179959849308383505167342565738382180666525211256221655129861213392455759272915565057394420728271409215556596974900718332893753172173500744392522771654048192448229319313386967045678744665093451560743782910263014930200762027209565313884859542996067229707388839912195826334964819133016500346618083969320902775088800287566711941842968839787149808739739233388585677095545116231323172342995837636586249573194609\",\"drivinglicenseid\":\"102840929811153624977554462471309185033977661854754815794111114507549576719389525167082631547450413573293352276930065480432301200611396989595571202142654033217842162456070556560693402484110499573693863745648118310258284468114751958738878996458420605301017450868522680454545537837403398645500541915771765220093329728663621098538954397330411649083351383375839056527007892276284168437065687748085384178113959961057476582871100422859953560730152958588610850909069434658487744782540788968302663076149478487413357533660817020800754493642858564081116318655661240523146995256712471572605700346459123074377380656921337264554594\",\"familyname\":\"2428690037146701497427424649573806616639612325136606164619283916796880313617677563507218774958436668407050506838114136163250163675016510113975582318007560622124292458766639319715064358235569650961433812439763343736699708535945693241909905707497180931492818502593885932421170612418693515054756633264933222189766691632082890045477718331705366111669009551578289182848340651375008362238266590844461708981816856194045325523248527964502118319210042254240848590574645476930113881493472578612352948284862674703949781070309344526122291448990325949065193279599181502524961004046979227803224474342778516917124487012958845744311\",\"master_secret\":\"96236339155824229583363924057798366491998077727991424922911165403434522806469328114407334094535810942859512352089785125683335350062474092708044674085769524387654467267128528564551803293661877480971961092735622606052503557881856409855812611523475975566606131897917979412576797874632169829901968854843162299366867885636535326810998541141840561418097240137120398317445832694001031827068485975315937269024666370665530455146256019590700349556357390218401217383173228376078058967743472704019765210324846681867991543267171763037513180046865961560351035005185946817643006206395175857900512245900162751815626427008481585714891\"},\"rctxt\":\"54359809198312125478916383106913469635175253891208897419510030559787479974126666313900084654632259260010008369569778456071591398552341004538623276997178295939490854663263886825856426285604332554317424030793691008221895556474599466123873279022389276698551452690414982831059651505731449763128921782866843113361548859434294057249048041670761184683271568216202174527891374770703485794299697663353847310928998125365841476766767508733046891626759537001358973715760759776149482147060701775948253839125589216812475133616408444838011643485797584321993661048373877626880635937563283836661934456534313802815974883441215836680800\",\"z\":\"99592262675748359673042256590146366586480829950402370244401571195191609039150608482506917768910598228167758026656953725016982562881531475875469671976107506976812319765644401707559997823702387678953647104105378063905395973550729717937712350758544336716556268064226491839700352305793370980462034813589488455836259737325502578253339820590260554457468082536249525493340350556649403477875367398139579018197084796440810685458274393317299082017275568964540311198115802021902455672385575542594821996060452628805634468222196284384514736044680778624637228114693554834388824212714580770066729185685978935409859595244639193538156\"}},\"issuerId\":\"did:prism:604ba1764ab89993f9a74625cc4f3e04737919639293eb382cc7adc53767f550\"}" diff --git a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/contacts/ContactsViewModel.kt b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/contacts/ContactsViewModel.kt index 902a3ce6e..542cb685a 100644 --- a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/contacts/ContactsViewModel.kt +++ b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/contacts/ContactsViewModel.kt @@ -8,6 +8,9 @@ import androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch import org.hyperledger.identus.walletsdk.domain.models.DIDPair import org.hyperledger.identus.walletsdk.edgeagent.EdgeAgentError +import org.hyperledger.identus.walletsdk.edgeagent.protocols.issueCredential.OfferCredential +import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.ConnectionlessCredentialOffer +import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.ConnectionlessRequestPresentation import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.OutOfBandInvitation import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.PrismOnboardingInvitation import org.hyperledger.identus.walletsdk.sampleapp.Sdk @@ -46,6 +49,21 @@ class ContactsViewModel(application: Application) : AndroidViewModel(application agent.acceptInvitation(invitation) } + is ConnectionlessCredentialOffer -> { + val offer = OfferCredential.fromMessage(invitation.offerCredential.makeMessage()) + val subjectDID = agent.createNewPrismDID() + val request = + agent.prepareRequestCredentialWithIssuer( + subjectDID, + offer + ) + agent.sendMessage(request.makeMessage()) + } + + is ConnectionlessRequestPresentation -> { + agent.pluto.storeMessage(invitation.requestPresentation.makeMessage()) + } + else -> { throw EdgeAgentError.UnknownInvitationTypeError(invitation.toString()) }