diff --git a/edge-agent-sdk/build.gradle.kts b/edge-agent-sdk/build.gradle.kts index 98da89c29..83083e36f 100644 --- a/edge-agent-sdk/build.gradle.kts +++ b/edge-agent-sdk/build.gradle.kts @@ -233,6 +233,7 @@ kotlin { exclude(group = "com.nimbusds", module = "nimbus-jose-jwt") } implementation(kotlin("reflect")) + implementation("org.bouncycastle:bcprov-jdk15on:1.68") implementation("com.apicatalog:titanium-json-ld-jre8:1.4.0") implementation("org.glassfish:jakarta.json:2.0.1") diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/apollo/ApolloImpl.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/apollo/ApolloImpl.kt index 11c67a738..89d6ea35b 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/apollo/ApolloImpl.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/apollo/ApolloImpl.kt @@ -38,7 +38,6 @@ import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PublicKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.RawKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.SeedKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.StorableKey -import org.hyperledger.identus.walletsdk.domain.models.keyManagement.StorablePrivateKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.TypeKey import org.hyperledger.identus.walletsdk.logger.LogComponent import org.hyperledger.identus.walletsdk.logger.LogLevel @@ -463,37 +462,6 @@ class ApolloImpl( } } - /** - * Restores a private key from StorablePrivateKey. - * - * @param storablePrivateKey The StorablePrivateKey to restore the key from. - * @return The restored Key object. - */ - @Deprecated( - "This method has been deprecated and should no longer be used.", - ReplaceWith("restorePrivateKey(restoreIdentifier, privateKeyData)"), - DeprecationLevel.ERROR - ) - override fun restorePrivateKey(storablePrivateKey: StorablePrivateKey): PrivateKey { - return when (storablePrivateKey.restorationIdentifier) { - "secp256k1+priv" -> { - Secp256k1PrivateKey(storablePrivateKey.data.base64UrlDecodedBytes) - } - - "ed25519+priv" -> { - Ed25519PrivateKey(storablePrivateKey.data.base64UrlDecodedBytes) - } - - "x25519+priv" -> { - X25519PrivateKey(storablePrivateKey.data.base64UrlDecodedBytes) - } - - else -> { - throw PlutoError.InvalidRestorationIdentifier() - } - } - } - /** * Restores a private key from StorablePrivateKey. * diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/apollo/utils/Ed25519PrivateKey.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/apollo/utils/Ed25519PrivateKey.kt index 574041d76..e8a52579b 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/apollo/utils/Ed25519PrivateKey.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/apollo/utils/Ed25519PrivateKey.kt @@ -1,6 +1,10 @@ package org.hyperledger.identus.walletsdk.apollo.utils -import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import org.bouncycastle.asn1.ASN1Encodable +import org.bouncycastle.asn1.DEROctetString +import org.bouncycastle.asn1.edec.EdECObjectIdentifiers +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo +import org.bouncycastle.asn1.x509.AlgorithmIdentifier import org.bouncycastle.jce.provider.BouncyCastleProvider import org.hyperledger.identus.apollo.base64.base64UrlEncoded import org.hyperledger.identus.apollo.utils.KMMEdPrivateKey @@ -128,9 +132,17 @@ class Ed25519PrivateKey(nativeValue: ByteArray) : PrivateKey(), SignableKey, Sto get() = "ed25519+priv" override fun jca(): java.security.PrivateKey { - val privateKeyParams = Ed25519PrivateKeyParameters(raw, 0) - val pkcs8Encoded = privateKeyParams.encoded - val keyFactory = KeyFactory.getInstance("Ed25519", BouncyCastleProvider()) - return keyFactory.generatePrivate(PKCS8EncodedKeySpec(pkcs8Encoded)) + val algId: AlgorithmIdentifier = AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519) + // Wrap the private key bytes in a DEROctetString + val privateKey: ASN1Encodable = DEROctetString(raw) + // Create the PrivateKeyInfo structure + val privateKeyInfo: PrivateKeyInfo = PrivateKeyInfo(algId, privateKey) + // Get the PKCS#8 encoded bytes + val pkcs8Bytes: ByteArray = privateKeyInfo.getEncoded() + val keySpec = PKCS8EncodedKeySpec(pkcs8Bytes) + // Get a KeyFactory for Ed25519 using Bouncy Castle + val keyFactory = KeyFactory.getInstance("ed25519", BouncyCastleProvider()) + // Generate the PrivateKey object + return keyFactory.generatePrivate(keySpec) } } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/CastorImpl.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/CastorImpl.kt index 67a348b62..172ecfdae 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/CastorImpl.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/CastorImpl.kt @@ -69,12 +69,14 @@ constructor( */ override fun createPrismDID( masterPublicKey: PublicKey, - services: Array? + services: Array?, + authenticationKeys: Array ): DID { return CastorShared.createPrismDID( apollo = apollo, masterPublicKey = masterPublicKey, - services = services + services = services, + authenticationKeys = authenticationKeys ) } @@ -123,7 +125,8 @@ constructor( try { val resolved = resolver.resolve(did) return resolved - } catch (_: CastorError) { + } catch (err: CastorError) { + println(err.message) } } throw Exception("No resolver could resolve the provided DID.") diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/did/prismdid/PrismDIDPublicKey.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/did/prismdid/PrismDIDPublicKey.kt index 14a4f0eca..f30a21b27 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/did/prismdid/PrismDIDPublicKey.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/did/prismdid/PrismDIDPublicKey.kt @@ -1,13 +1,22 @@ package org.hyperledger.identus.walletsdk.castor.did.prismdid -import org.hyperledger.identus.apollo.secp256k1.Secp256k1Lib +import org.hyperledger.identus.apollo.base64.base64UrlDecodedBytes +import org.hyperledger.identus.apollo.base64.base64UrlEncoded import org.hyperledger.identus.protos.CompressedECKeyData +import org.hyperledger.identus.protos.ECKeyData import org.hyperledger.identus.protos.KeyUsage +import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519PublicKey import org.hyperledger.identus.walletsdk.apollo.utils.Secp256k1PublicKey import org.hyperledger.identus.walletsdk.domain.buildingblocks.Apollo import org.hyperledger.identus.walletsdk.domain.models.CastorError import org.hyperledger.identus.walletsdk.domain.models.Curve +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.CurveKey +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.CurvePointXKey +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.CurvePointYKey +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.KeyTypes import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PublicKey +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.RawKey +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.TypeKey import pbandk.ByteArr import kotlin.jvm.Throws @@ -54,9 +63,27 @@ class PrismDIDPublicKey { this.usage = proto.usage.fromProto() this.keyData = when (proto.keyData) { is org.hyperledger.identus.protos.PublicKey.KeyData.CompressedEcKeyData -> { - Secp256k1PublicKey(proto.keyData.value.data.array) + val compressedEcKeyData = proto.compressedEcKeyData!! + apollo.createPublicKey( + properties = mapOf( + TypeKey().property to KeyTypes.EC, + CurveKey().property to compressedEcKeyData.curve, + RawKey().property to compressedEcKeyData.data.array + ) + ) } + is org.hyperledger.identus.protos.PublicKey.KeyData.EcKeyData -> { + val ecKeyData = proto.ecKeyData!! + apollo.createPublicKey( + properties = mapOf( + TypeKey().property to KeyTypes.EC, + CurveKey().property to ecKeyData.curve, + CurvePointXKey().property to ecKeyData.x.array.base64UrlEncoded, + CurvePointYKey().property to ecKeyData.y.array.base64UrlEncoded + ) + ) + } else -> { throw CastorError.InvalidPublicKeyEncoding("prism", "secp256k1") } @@ -69,14 +96,33 @@ class PrismDIDPublicKey { * @return the converted Protobuf PublicKey object */ fun toProto(): org.hyperledger.identus.protos.PublicKey { - val compressedPublicKey = Secp256k1PublicKey(Secp256k1Lib().compressPublicKey(keyData.getValue())) - return org.hyperledger.identus.protos.PublicKey( - id = id, - usage = usage.toProto(), - keyData = org.hyperledger.identus.protos.PublicKey.KeyData.CompressedEcKeyData( - compressedPublicKey.toProto() + if (keyData.getCurve() == Curve.SECP256K1.value) { + val x = keyData.getProperty(CurvePointXKey().property) + val y = keyData.getProperty(CurvePointYKey().property) + + return org.hyperledger.identus.protos.PublicKey( + id = id, + usage = usage.toProto(), + keyData = org.hyperledger.identus.protos.PublicKey.KeyData.EcKeyData( + ecKeyData = ECKeyData( + curve = keyData.getCurve(), + x = ByteArr(x.base64UrlDecodedBytes), + y = ByteArr(y.base64UrlDecodedBytes) + ) + ) ) - ) + } else { + return org.hyperledger.identus.protos.PublicKey( + id = id, + usage = usage.toProto(), + keyData = org.hyperledger.identus.protos.PublicKey.KeyData.CompressedEcKeyData( + compressedEcKeyData = CompressedECKeyData( + curve = Curve.ED25519.value, + data = ByteArr(keyData.raw) + ) + ) + ) + } } /** @@ -128,6 +174,18 @@ fun Secp256k1PublicKey.toProto(): CompressedECKeyData { ) } +/** + * Converts a Ed25519PublicKey object to a CompressedECKeyData object. + * + * @return the converted CompressedECKeyData object. + */ +fun Ed25519PublicKey.toProto(): CompressedECKeyData { + return CompressedECKeyData( + curve = Curve.ED25519.value, + data = ByteArr(raw) + ) +} + /** * Generates the identifier for a PrismDIDPublicKey.Usage based on the given index. * diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/shared/CastorShared.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/shared/CastorShared.kt index bfc976a57..0ca527ca0 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/shared/CastorShared.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/castor/shared/CastorShared.kt @@ -92,26 +92,36 @@ internal class CastorShared { internal fun createPrismDID( apollo: Apollo, masterPublicKey: PublicKey, - services: Array? + services: Array?, + authenticationKeys: Array ): DID { + val pks: MutableList = mutableListOf() + + pks.add( + PrismDIDPublicKey( + apollo = apollo, + id = PrismDIDPublicKey.Usage.MASTER_KEY.defaultId(), + usage = PrismDIDPublicKey.Usage.MASTER_KEY, + keyData = masterPublicKey + ).toProto() + ) + + // Add a public key for each authentication key + for (authKey in authenticationKeys) { + pks.add( + PrismDIDPublicKey( + apollo = apollo, + id = PrismDIDPublicKey.Usage.AUTHENTICATION_KEY.defaultId(), + usage = PrismDIDPublicKey.Usage.AUTHENTICATION_KEY, + keyData = authKey + ).toProto() + ) + } val atalaOperation = AtalaOperation( operation = AtalaOperation.Operation.CreateDid( CreateDIDOperation( didData = CreateDIDOperation.DIDCreationData( - publicKeys = listOf( - PrismDIDPublicKey( - apollo = apollo, - id = PrismDIDPublicKey.Usage.MASTER_KEY.defaultId(), - usage = PrismDIDPublicKey.Usage.MASTER_KEY, - keyData = masterPublicKey - ).toProto(), - PrismDIDPublicKey( - apollo = apollo, - id = PrismDIDPublicKey.Usage.AUTHENTICATION_KEY.defaultId(), - usage = PrismDIDPublicKey.Usage.AUTHENTICATION_KEY, - keyData = masterPublicKey - ).toProto() - ), + publicKeys = pks, services = services?.map { Service( id = it.id, @@ -123,7 +133,6 @@ internal class CastorShared { ) ) ) - val encodedState = atalaOperation.encodeToByteArray() val stateHash = SHA256().digest(encodedState).toHexString() val base64State = encodedState.base64UrlEncoded diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Castor.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Castor.kt index 511cb82e4..b1db0ea78 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Castor.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Castor.kt @@ -34,7 +34,8 @@ interface Castor { */ fun createPrismDID( masterPublicKey: PublicKey, - services: Array? + services: Array?, + authenticationKeys: Array ): DID /** diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Pluto.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Pluto.kt index 4822c13db..4dfd6e2b5 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Pluto.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/buildingblocks/Pluto.kt @@ -106,19 +106,6 @@ interface Pluto { */ fun storeLinkSecret(linkSecret: String) - /** - * Stores the metadata associated with a credential request. - * - * @param name the unique name used to retrieve the stored metadata. - * @param metadata The metadata to store. It must be an instance of [CredentialRequestMeta]. - */ - @Deprecated( - "This method has been deprecated and should no longer be used.", - ReplaceWith("storeCredentialMetadata(name, linkSecretName, json)"), - DeprecationLevel.ERROR - ) - fun storeCredentialMetadata(name: String, metadata: CredentialRequestMeta) - /** * Stores the metadata associated with a credential request. * diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/ProvableCredential.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/ProvableCredential.kt index 5f4e68bbf..034a10265 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/ProvableCredential.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/ProvableCredential.kt @@ -1,6 +1,7 @@ package org.hyperledger.identus.walletsdk.domain.models import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.SignableKey as SignableKeyModel sealed class CredentialOperationsOptions { data class Schema(val id: String, val json: String) : CredentialOperationsOptions() @@ -10,7 +11,7 @@ sealed class CredentialOperationsOptions { data class LinkSecret(val id: String, val secret: String) : CredentialOperationsOptions() data class SubjectDID(val did: DID) : CredentialOperationsOptions() data class Entropy(val entropy: String) : CredentialOperationsOptions() - data class SignableKey(val key: SignableKey?) : CredentialOperationsOptions() + data class SignableKey(val key: SignableKeyModel?) : CredentialOperationsOptions() data class ExportableKey(val key: PrivateKey?) : CredentialOperationsOptions() data class ZkpPresentationParams(val attributes: Map, val predicates: List) : CredentialOperationsOptions() @@ -20,5 +21,5 @@ sealed class CredentialOperationsOptions { } interface ProvableCredential { - suspend fun presentation(request: ByteArray, options: List): String + suspend fun presentation(attachmentFormat: String, request: ByteArray, options: List): String } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/VerifiableCredential.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/VerifiableCredential.kt index 3574cae20..2591c8dce 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/VerifiableCredential.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/VerifiableCredential.kt @@ -104,17 +104,25 @@ sealed interface VerifiableCredential { } } -sealed interface PresentationClaims +sealed interface PresentationClaims { + val claims: Map +} data class JWTPresentationClaims( val schema: String? = null, val issuer: String? = null, - val claims: Map + override val claims: Map +) : PresentationClaims + +data class SDJWTPresentationClaims( + val issuer: String? = null, + override val claims: Map ) : PresentationClaims data class AnoncredsPresentationClaims( val predicates: Map, - val attributes: Map + val attributes: Map, + override val claims: Map = emptyMap() ) : PresentationClaims @OptIn(ExperimentalSerializationApi::class) diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/keyManagement/KeyRestoration.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/keyManagement/KeyRestoration.kt index ec5423288..e6f59fe2f 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/keyManagement/KeyRestoration.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/domain/models/keyManagement/KeyRestoration.kt @@ -44,19 +44,6 @@ interface KeyRestoration { */ fun restoreKey(key: JWK, index: Int? = null): Key - /** - * Restores a private key from StorablePrivateKey. - * - * @param storablePrivateKey The StorablePrivateKey to restore the key from. - * @return The restored Key object. - */ - @Deprecated( - "This method has been deprecated and should no longer be used.", - ReplaceWith("restorePrivateKey(restoreIdentifier, privateKeyData)"), - DeprecationLevel.ERROR - ) - fun restorePrivateKey(storablePrivateKey: StorablePrivateKey): PrivateKey - /** * Restores a private key using a restoration identifier and private key data encoded in base 64. * 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 27bc02fd9..bb8444fef 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 @@ -84,6 +84,7 @@ import org.hyperledger.identus.walletsdk.domain.models.UnknownError import org.hyperledger.identus.walletsdk.domain.models.httpClient import org.hyperledger.identus.walletsdk.domain.models.keyManagement.CurveKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.DerivationPathKey +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.IndexKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.KeyPair import org.hyperledger.identus.walletsdk.domain.models.keyManagement.KeyTypes import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey @@ -109,20 +110,25 @@ import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.OutOfBand import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.PrismOnboardingInvitation import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.AnoncredsPresentationOptions import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.JWTPresentationOptions +import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.PreparePresentationOptions import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.Presentation import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.PresentationSubmissionOptionsAnoncreds import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.PresentationSubmissionOptionsJWT import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.RequestPresentation +import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.SDJWTPreparePresentationOptions +import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.SDJWTPresentationOptions import org.hyperledger.identus.walletsdk.logger.LogComponent -import org.hyperledger.identus.walletsdk.logger.Metadata import org.hyperledger.identus.walletsdk.logger.Logger import org.hyperledger.identus.walletsdk.logger.LoggerImpl +import org.hyperledger.identus.walletsdk.logger.Metadata import org.hyperledger.identus.walletsdk.pluto.PlutoBackupTask import org.hyperledger.identus.walletsdk.pluto.PlutoRestoreTask import org.hyperledger.identus.walletsdk.pluto.models.backup.BackupV0_0_1 import org.hyperledger.identus.walletsdk.pollux.models.AnoncredsPresentationDefinitionRequest import org.hyperledger.identus.walletsdk.pollux.models.CredentialRequestMeta +import org.hyperledger.identus.walletsdk.pollux.models.JWTCredential import org.hyperledger.identus.walletsdk.pollux.models.JWTPresentationDefinitionRequest +import org.hyperledger.identus.walletsdk.pollux.models.SDJWTCredential import org.kotlincrypto.hash.sha2.SHA256 /** @@ -355,7 +361,6 @@ open class EdgeAgent { logger.info(message = "Agent not running") } - // DID Higher Functions /** * This method create a new Prism DID, that can be used to identify the agent and interact with other agents. * @@ -371,9 +376,36 @@ open class EdgeAgent { services: Array = emptyArray() ): DID { val index = keyPathIndex ?: (pluto.getPrismLastKeyPathIndex().first() + 1) - val keyPair = Secp256k1KeyPair.generateKeyPair(seed, KeyCurve(Curve.SECP256K1, index)) - val did = castor.createPrismDID(masterPublicKey = keyPair.publicKey, services = services) - registerPrismDID(did, index, alias, keyPair.privateKey) + + val secp256k1PrivateKeyProperties: MutableMap = mutableMapOf() + secp256k1PrivateKeyProperties[TypeKey().property] = KeyTypes.EC + secp256k1PrivateKeyProperties[SeedKey().property] = seed.value.base64UrlEncoded + secp256k1PrivateKeyProperties[CurveKey().property] = Curve.SECP256K1.value + secp256k1PrivateKeyProperties[IndexKey().property] = index + + val ed25519PrivateKeyProperties: MutableMap = mutableMapOf() + + ed25519PrivateKeyProperties[TypeKey().property] = KeyTypes.EC + ed25519PrivateKeyProperties[SeedKey().property] = seed.value.base64UrlEncoded + ed25519PrivateKeyProperties[CurveKey().property] = Curve.ED25519.value + ed25519PrivateKeyProperties[IndexKey().property] = index + 1 + + val masterKey = this.apollo.createPrivateKey( + secp256k1PrivateKeyProperties + ) as Secp256k1PrivateKey + + val authenticationKey = this.apollo.createPrivateKey( + ed25519PrivateKeyProperties + ) as Ed25519PrivateKey + + val did = castor.createPrismDID( + masterPublicKey = masterKey.publicKey(), + services = services, + authenticationKeys = arrayOf( + authenticationKey.publicKey() + ) + ) + registerPrismDID(did, index, alias, masterKey) return did } @@ -650,19 +682,24 @@ open class EdgeAgent { } return when (val type = pollux.extractCredentialFormatFromMessage(offer.attachments)) { - CredentialType.JWT -> { + CredentialType.JWT, CredentialType.SDJWT -> { val privateKeyKeyPath = pluto.getPrismDIDKeyPathIndex(did).first() - val keyPair = Secp256k1KeyPair.generateKeyPair( - seed, - KeyCurve(Curve.SECP256K1, privateKeyKeyPath) - ) val offerDataString = offer.attachments.firstNotNullOf { it.data.getDataAsJsonString() } val offerJsonObject = Json.parseToJsonElement(offerDataString).jsonObject - val jwtString = + + val jwtString = if (type == CredentialType.JWT) { + val keyPair = Secp256k1KeyPair.generateKeyPair( + seed, + KeyCurve(Curve.SECP256K1, privateKeyKeyPath) + ) pollux.processCredentialRequestJWT(did, keyPair.privateKey, offerJsonObject) + } else { + val keyPair = Ed25519KeyPair.generateKeyPair() + pollux.processCredentialRequestSDJWT(did, keyPair.privateKey, offerJsonObject) + } val attachmentDescriptor = AttachmentDescriptor( mediaType = ContentType.Application.Json.toString(), @@ -971,7 +1008,8 @@ open class EdgeAgent { @Throws(PolluxError.InvalidPrismDID::class, EdgeAgentError.CredentialNotValidForPresentationRequest::class) suspend fun preparePresentationForRequestProof( request: RequestPresentation, - credential: T + credential: T, + preparePresentationOptions: PreparePresentationOptions? = null ): Presentation where T : Credential, T : ProvableCredential { val attachmentFormat = request.attachments.first().format ?: CredentialType.Unknown.type // Presentation request from agent @@ -980,7 +1018,7 @@ open class EdgeAgent { when (attachmentFormat) { CredentialType.PRESENTATION_EXCHANGE_DEFINITIONS.type -> { // Presentation Exchange - return handlePresentationDefinitionRequest(request, credential) + return handlePresentationDefinitionRequest(request, credential, preparePresentationOptions) } CredentialType.ANONCREDS_PROOF_REQUEST.type -> { @@ -994,6 +1032,7 @@ open class EdgeAgent { val linkSecret = getLinkSecret() try { presentationString = credential.presentation( + attachmentFormat, requestData.encodeToByteArray(), listOf( CredentialOperationsOptions.LinkSecret("", linkSecret), @@ -1025,6 +1064,7 @@ open class EdgeAgent { } try { presentationString = credential.presentation( + attachmentFormat, requestData.encodeToByteArray(), listOf( CredentialOperationsOptions.SubjectDID(subjectDID), @@ -1038,13 +1078,35 @@ open class EdgeAgent { } CredentialType.SDJWT.type -> { + if (preparePresentationOptions == null) { + throw EdgeAgentError.MissingOrNullFieldError( + "presentationFrame", + "preparePresentationOptions" + ) + } + + if (preparePresentationOptions !is SDJWTPreparePresentationOptions) { + throw EdgeAgentError.InvalidPresentationOptions( + preparePresentationOptions::class.java.name, + SDJWTPreparePresentationOptions::class.java.name + ) + } + val requestData = request.attachments.firstNotNullOf { it.data.getDataAsJsonString() } + + val disclosingClaims = mutableListOf() + preparePresentationOptions.presentationFrame.forEach { presentationFrame -> + if (presentationFrame.value) { + disclosingClaims.add(presentationFrame.key) + } + } try { presentationString = credential.presentation( + attachmentFormat, requestData.encodeToByteArray(), - listOf(CredentialOperationsOptions.DisclosingClaims(listOf(credential.claims.toString()))) + listOf(CredentialOperationsOptions.DisclosingClaims(disclosingClaims)) ) } catch (e: Exception) { throw EdgeAgentError.CredentialNotValidForPresentationRequest() @@ -1092,9 +1154,11 @@ open class EdgeAgent { throw EdgeAgentError.MissingOrNullFieldError("Domain", "initiatePresentationRequest parameters") } if (challenge == null) { - throw EdgeAgentError.MissingOrNullFieldError("Challenge", "initiatePresentationRequest parameters") + throw EdgeAgentError.MissingOrNullFieldError( + "Challenge", + "initiatePresentationRequest parameters" + ) } - presentationDefinitionRequest = pollux.createPresentationDefinitionRequest( type = type, presentationClaims = presentationClaims, @@ -1111,6 +1175,21 @@ open class EdgeAgent { ) } + CredentialType.SDJWT -> { + presentationDefinitionRequest = pollux.createPresentationDefinitionRequest( + type = type, + presentationClaims = presentationClaims, + options = SDJWTPresentationOptions( + sdjwt = arrayOf("ES256k") + ) + ) + attachmentDescriptor = AttachmentDescriptor( + mediaType = "application/json", + format = CredentialType.PRESENTATION_EXCHANGE_DEFINITIONS.type, + data = AttachmentBase64(presentationDefinitionRequest.base64UrlEncoded) + ) + } + CredentialType.ANONCREDS_PROOF_REQUEST -> { presentationDefinitionRequest = pollux.createPresentationDefinitionRequest( type = type, @@ -1147,7 +1226,8 @@ open class EdgeAgent { private suspend fun handlePresentationDefinitionRequest( requestPresentation: RequestPresentation, - credential: Credential + credential: Credential, + preparePresentationOptions: PreparePresentationOptions? = null ): Presentation { try { if (credential !is ProvableCredential) { @@ -1156,60 +1236,135 @@ open class EdgeAgent { val presentationDefinitionRequestString = requestPresentation.attachments.firstNotNullOf { it.data.getDataAsJsonString() } - if (presentationDefinitionRequestString.contains("jwt")) { - // If the json can be used to instantiate a JWTPresentationDefinitionRequest, process the request - // as JWT. - val didString = - credential.subject ?: throw Exception("Credential must contain subject") - - val storablePrivateKey = pluto.getDIDPrivateKeysByDID(DID(didString)).first().first() - ?: throw EdgeAgentError.CannotFindDIDPrivateKey(didString) - val privateKey = - apollo.restorePrivateKey(storablePrivateKey.restorationIdentifier, storablePrivateKey.data) - - val presentationSubmissionProof = pollux.createJWTPresentationSubmission( - presentationDefinitionRequest = presentationDefinitionRequestString, - credential = credential, - privateKey = privateKey, - ) - - val attachmentDescriptor = AttachmentDescriptor( - mediaType = "application/json", - format = CredentialType.PRESENTATION_EXCHANGE_SUBMISSION.type, - data = AttachmentBase64(presentationSubmissionProof.base64UrlEncoded) - ) + val json = Json.parseToJsonElement(presentationDefinitionRequestString).jsonObject + if (json.containsKey("presentation_definition")) { + val presentationDefinition = json["presentation_definition"]!!.jsonObject val fromDID = requestPresentation.to ?: createNewPeerDID(updateMediator = true) - return Presentation( - body = Presentation.Body(), - attachments = arrayOf(attachmentDescriptor), - thid = requestPresentation.thid ?: requestPresentation.id, - from = fromDID, - to = requestPresentation.from - ) - } else { - val linkSecret = getLinkSecret() + if (presentationDefinition.containsKey("format")) { + val format = presentationDefinition["format"]!!.jsonObject + if (format["jwt"] != null) { + if (credential !is JWTCredential) { + throw EdgeAgentError.CredentialNotValidForPresentationRequest() + } + // If the json can be used to instantiate a JWTPresentationDefinitionRequest, process the request + // as JWT. + val didString = + credential.subject ?: throw Exception("Credential must contain subject") + + val storablePrivateKey = pluto.getDIDPrivateKeysByDID(DID(didString)).first().first() + ?: throw EdgeAgentError.CannotFindDIDPrivateKey(didString) + val privateKey = + apollo.restorePrivateKey(storablePrivateKey.restorationIdentifier, storablePrivateKey.data) + + val presentationString = credential.presentation( + CredentialType.PRESENTATION_EXCHANGE_DEFINITIONS.type, + presentationDefinitionRequestString.encodeToByteArray(), + listOf( + CredentialOperationsOptions.SubjectDID(DID(didString)), + CredentialOperationsOptions.ExportableKey(privateKey) + ) + ) - val presentationSubmissionProof = pollux.createAnoncredsPresentationSubmission( - presentationDefinitionRequest = presentationDefinitionRequestString, - credential = credential, - linkSecret = linkSecret - ) + val attachmentDescriptor = AttachmentDescriptor( + mediaType = "application/json", + format = CredentialType.PRESENTATION_EXCHANGE_SUBMISSION.type, + data = AttachmentBase64(presentationString.base64UrlEncoded) + ) - val attachmentDescriptor = AttachmentDescriptor( - mediaType = "application/json", - format = CredentialType.PRESENTATION_EXCHANGE_SUBMISSION.type, - data = AttachmentBase64(presentationSubmissionProof.base64UrlEncoded) - ) - val fromDID = requestPresentation.to ?: createNewPeerDID(updateMediator = true) - return Presentation( - body = Presentation.Body(), - attachments = arrayOf(attachmentDescriptor), - thid = requestPresentation.thid ?: requestPresentation.id, - from = fromDID, - to = requestPresentation.from - ) + return Presentation( + body = Presentation.Body(), + attachments = arrayOf(attachmentDescriptor), + thid = requestPresentation.thid ?: requestPresentation.id, + from = fromDID, + to = requestPresentation.from + ) + } else if (format["sdJwt"] != null) { + if (credential !is SDJWTCredential) { + throw EdgeAgentError.CredentialNotValidForPresentationRequest() + } + + if (preparePresentationOptions == null) { + throw EdgeAgentError.MissingOrNullFieldError( + "presentationFrame", + "preparePresentationOptions" + ) + } + + if (preparePresentationOptions !is SDJWTPreparePresentationOptions) { + throw EdgeAgentError.InvalidPresentationOptions( + preparePresentationOptions::class.java.name, + SDJWTPreparePresentationOptions::class.java.name + ) + } + + val didString = + credential.subject ?: throw Exception("Credential must contain subject") + + val storablePrivateKey = pluto.getDIDPrivateKeysByDID(DID(didString)).first().first() + ?: throw EdgeAgentError.CannotFindDIDPrivateKey(didString) + val privateKey = + apollo.restorePrivateKey(storablePrivateKey.restorationIdentifier, storablePrivateKey.data) + + val disclosingClaims = mutableListOf() + preparePresentationOptions.presentationFrame.forEach { presentationFrame -> + if (presentationFrame.value) { + disclosingClaims.add(presentationFrame.key) + } + } + + val presentationString = credential.presentation( + CredentialType.PRESENTATION_EXCHANGE_DEFINITIONS.type, + presentationDefinitionRequestString.encodeToByteArray(), + listOf( + CredentialOperationsOptions.DisclosingClaims(disclosingClaims), + CredentialOperationsOptions.SubjectDID(DID(didString)) + ) + ) + + val attachmentDescriptor = AttachmentDescriptor( + mediaType = "application/json", + format = CredentialType.PRESENTATION_EXCHANGE_SUBMISSION.type, + data = AttachmentBase64(presentationString.base64UrlEncoded) + ) + + return Presentation( + body = Presentation.Body(), + attachments = arrayOf(attachmentDescriptor), + thid = requestPresentation.thid ?: requestPresentation.id, + from = fromDID, + to = requestPresentation.from + ) + } else { + val linkSecret = getLinkSecret() + + val presentationSubmissionProof = credential.presentation( + CredentialType.ANONCREDS_PROOF_REQUEST.type, + presentationDefinitionRequestString.toByteArray(), + listOf( + CredentialOperationsOptions.LinkSecret("", linkSecret), + CredentialOperationsOptions.SchemaDownloader(api), + CredentialOperationsOptions.CredentialDefinitionDownloader(api) + ) + ) + + val attachmentDescriptor = AttachmentDescriptor( + mediaType = "application/json", + format = CredentialType.PRESENTATION_EXCHANGE_SUBMISSION.type, + data = AttachmentBase64(presentationSubmissionProof.base64UrlEncoded) + ) + + return Presentation( + body = Presentation.Body(), + attachments = arrayOf(attachmentDescriptor), + thid = requestPresentation.thid ?: requestPresentation.id, + from = fromDID, + to = requestPresentation.from + ) + } + } } + throw EdgeAgentError.CredentialNotValidForPresentationRequest() } catch (e: Exception) { throw EdgeAgentError.CredentialNotValidForPresentationRequest() } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgentError.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgentError.kt index afce6e5c2..f2c7065da 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgentError.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/EdgeAgentError.kt @@ -152,4 +152,13 @@ sealed class EdgeAgentError : KnownPrismError() { override val message: String get() = "This invitation has expired." } + + class InvalidPresentationOptions(private val type: String, private val expectedType: String) : + EdgeAgentError() { + override val code: Int + get() = 616 + + override val message: String + get() = "Invalid presentation, got $type but expected $expectedType" + } } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/JWTPresentationOptions.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/JWTPresentationOptions.kt index 920630860..43100c8f8 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/JWTPresentationOptions.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/JWTPresentationOptions.kt @@ -6,11 +6,13 @@ import org.hyperledger.identus.walletsdk.domain.models.CredentialType sealed interface PresentationOptions { val type: CredentialType + val name: String? + val purpose: String? } data class JWTPresentationOptions( - val name: String? = "Presentation", - val purpose: String = "Presentation definition", + override val name: String = "Presentation", + override val purpose: String = "Presentation definition", val jwt: Array = arrayOf("ES256K"), val domain: String, val challenge: String @@ -40,9 +42,37 @@ data class JWTPresentationOptions( } } +data class SDJWTPresentationOptions( + override val name: String? = "Presentation", + override val purpose: String = "Presentation definition", + val sdjwt: Array = arrayOf("ES256K") +) : PresentationOptions { + override val type: CredentialType = CredentialType.SDJWT + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as SDJWTPresentationOptions + + if (name != other.name) return false + if (purpose != other.purpose) return false + return sdjwt.contentEquals(other.sdjwt) + } + + override fun hashCode(): Int { + var result = name?.hashCode() ?: 0 + result = 31 * result + purpose.hashCode() + result = 31 * result + (sdjwt.contentHashCode()) + return result + } +} + data class AnoncredsPresentationOptions( // TODO: This should be a nonce from the anoncred wrapper - val nonce: String + val nonce: String, + override val name: String? = null, + override val purpose: String? = null ) : PresentationOptions { override val type: CredentialType = CredentialType.ANONCREDS_PROOF_REQUEST } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/PreparePresentationOptions.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/PreparePresentationOptions.kt new file mode 100644 index 000000000..bac7144b3 --- /dev/null +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/PreparePresentationOptions.kt @@ -0,0 +1,7 @@ +package org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation + +sealed interface PreparePresentationOptions + +data class SDJWTPreparePresentationOptions( + val presentationFrame: Map +) : PreparePresentationOptions diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/PresentationSubmissionOptions.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/PresentationSubmissionOptions.kt index 5c3fb18b0..b9f54043f 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/PresentationSubmissionOptions.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/edgeagent/protocols/proofOfPresentation/PresentationSubmissionOptions.kt @@ -6,6 +6,10 @@ data class PresentationSubmissionOptionsJWT( val presentationDefinitionRequest: String ) : PresentationSubmissionOptions +data class PresentationSubmissionOptionsSDJWT( + val presentationDefinitionRequest: String +) : PresentationSubmissionOptions + data class PresentationSubmissionOptionsAnoncreds( val presentationDefinitionRequest: String ) : PresentationSubmissionOptions diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/CredentialRecovery.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/CredentialRecovery.kt index c7a6cc517..7254b12cf 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/CredentialRecovery.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/CredentialRecovery.kt @@ -20,7 +20,8 @@ class CredentialRecovery(val restorationId: String, val credentialData: ByteArra enum class RestorationID(val value: String) { JWT("jwt+credential"), ANONCRED("anon+credential"), - W3C("w3c+credential"); + W3C("w3c+credential"), + SDJWT("sd-jwt+credential"); /** * Converts a RestorationID object to a BackUpRestorationId object from the PlutoRestoreTask class. @@ -32,6 +33,7 @@ enum class RestorationID(val value: String) { JWT -> PlutoRestoreTask.BackUpRestorationId.JWT ANONCRED -> PlutoRestoreTask.BackUpRestorationId.ANONCRED W3C -> PlutoRestoreTask.BackUpRestorationId.W3C + SDJWT -> PlutoRestoreTask.BackUpRestorationId.SDJWT } } } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoBackupTask.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoBackupTask.kt index 5fef885d6..3a3428ce2 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoBackupTask.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoBackupTask.kt @@ -87,6 +87,10 @@ open class PlutoBackupTask(private val pluto: Pluto) { PlutoRestoreTask.BackUpRestorationId.W3C } + RestorationID.SDJWT.value -> { + PlutoRestoreTask.BackUpRestorationId.SDJWT + } + else -> { throw UnknownError.SomethingWentWrongError("Unknown restoration ID ${credentialRecovery.restorationId}") } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoImpl.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoImpl.kt index b20485109..3b8980e0d 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoImpl.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoImpl.kt @@ -382,28 +382,6 @@ class PlutoImpl( .insert(LinkSecretDB(linkSecret)) } - /** - * Stores the metadata associated with a credential request. - * - * @param name the unique name used to retrieve the stored metadata. - * @param metadata The metadata to store. It must be an instance of [CredentialRequestMeta]. - * - * @deprecated This method has been deprecated and should no longer be used. - * @see storeCredentialMetadata(name, linkSecretName, json) for the replacement method that should be used. - */ - @Deprecated( - "This method has been deprecated and should no longer be used.", - ReplaceWith("storeCredentialMetadata(name, linkSecretName, json)"), - DeprecationLevel.ERROR - ) - override fun storeCredentialMetadata(name: String, metadata: CredentialRequestMeta) { - getInstance().credentialMetadataQueries.insert( - id = name, - linkSecretName = metadata.linkSecretName, - json = metadata.json - ) - } - /** * Stores the metadata associated with a credential request. * diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoRestoreTask.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoRestoreTask.kt index 769717278..cbdcea637 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoRestoreTask.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pluto/PlutoRestoreTask.kt @@ -44,6 +44,7 @@ import org.hyperledger.identus.walletsdk.pluto.PlutoRestoreTask.BackUpMessage.Js import org.hyperledger.identus.walletsdk.pluto.models.backup.BackupV0_0_1 import org.hyperledger.identus.walletsdk.pollux.models.AnonCredential import org.hyperledger.identus.walletsdk.pollux.models.JWTCredential +import org.hyperledger.identus.walletsdk.pollux.models.SDJWTCredential import kotlin.time.DurationUnit import kotlin.time.toDuration @@ -96,6 +97,12 @@ open class PlutoRestoreTask( .toAnonCredential().toStorableCredential() } + BackUpRestorationId.SDJWT.value -> { + val sdjwtString = it.data.base64UrlDecoded + .replace("\"null\"", "null") + SDJWTCredential.fromSDJwtString(sdjwtString).toStorableCredential() + } + else -> { throw UnknownError.SomethingWentWrongError("Unknown recovery id: ${it.recoveryId}") } @@ -336,7 +343,8 @@ open class PlutoRestoreTask( enum class BackUpRestorationId(val value: String) { JWT("jwt"), ANONCRED("anoncred"), - W3C("w3c"); + W3C("w3c"), + SDJWT("sd-jwt+credential"); /** * Converts a BackUpRestorationId object to a RestorationID object from the RestorationID class. @@ -348,6 +356,7 @@ open class PlutoRestoreTask( JWT -> RestorationID.JWT ANONCRED -> RestorationID.ANONCRED W3C -> RestorationID.W3C + SDJWT -> RestorationID.SDJWT } } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/DescriptorPath.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/DescriptorPath.kt index 487a4f8d3..f079b4a44 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/DescriptorPath.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/DescriptorPath.kt @@ -18,12 +18,15 @@ import kotlinx.serialization.json.longOrNull class DescriptorPath(private val jsonElement: JsonElement) { fun getValue(path: String): Any? { - val regex = Regex("[\\[\\]`.]+") - val segments = path.split(regex).filter { it.isNotEmpty() }.drop(1) + val regex = Regex("[/\\[\\]`.]+") + val segments = path.split(regex).filter { it.isNotEmpty() } + + // Drop the first segment if it starts with "$" + val filteredSegments = if (path.startsWith("$")) segments.drop(1) else segments val jsonObject = jsonElement.jsonObject var current: Any? = jsonObject - segments.forEach { segment -> + filteredSegments.forEach { segment -> when (current) { is JsonObject -> { if ((current as JsonObject).contains(segment)) { diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImpl.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImpl.kt index 3f1c96a18..feea02bbe 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImpl.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImpl.kt @@ -18,10 +18,22 @@ import com.nimbusds.jose.JWSAlgorithm import com.nimbusds.jose.JWSHeader import com.nimbusds.jose.crypto.ECDSASigner import com.nimbusds.jose.crypto.ECDSAVerifier +import com.nimbusds.jose.crypto.Ed25519Verifier import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.jwk.OctetKeyPair import com.nimbusds.jose.util.Base64URL import com.nimbusds.jwt.JWTClaimsSet import com.nimbusds.jwt.SignedJWT +import eu.europa.ec.eudi.sdjwt.JwtSignatureVerifier +import eu.europa.ec.eudi.sdjwt.KeyBindingVerifier +import eu.europa.ec.eudi.sdjwt.NoSignatureValidation +import eu.europa.ec.eudi.sdjwt.SdJwtVerifier +import eu.europa.ec.eudi.sdjwt.recreateClaimsAndDisclosuresPerClaim +import io.iohk.atala.prism.didcomm.didpeer.base64.base64UrlDecoded +import io.iohk.atala.prism.didcomm.didpeer.base64.base64UrlDecodedBytes +import io.iohk.atala.prism.didcomm.didpeer.base64.base64UrlEncoded +import io.iohk.atala.prism.didcomm.didpeer.core.toJsonElement import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.setl.rdf.normalization.RdfNormalize @@ -44,12 +56,17 @@ import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import org.bouncycastle.asn1.ASN1Encoding +import org.bouncycastle.asn1.edec.EdECObjectIdentifiers +import org.bouncycastle.asn1.x509.AlgorithmIdentifier +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters +import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey +import org.bouncycastle.jcajce.interfaces.EdDSAPublicKey import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.spec.ECNamedCurveSpec import org.didcommx.didcomm.common.Typ -import org.hyperledger.identus.apollo.base64.base64UrlDecoded -import org.hyperledger.identus.apollo.base64.base64UrlDecodedBytes import org.hyperledger.identus.apollo.utils.KMMECSecp256k1PublicKey import org.hyperledger.identus.walletsdk.apollo.helpers.gunzip import org.hyperledger.identus.walletsdk.apollo.utils.Secp256k1PrivateKey @@ -76,6 +93,7 @@ import org.hyperledger.identus.walletsdk.domain.models.KeyValue import org.hyperledger.identus.walletsdk.domain.models.PolluxError import org.hyperledger.identus.walletsdk.domain.models.PresentationClaims import org.hyperledger.identus.walletsdk.domain.models.RequestedPredicates +import org.hyperledger.identus.walletsdk.domain.models.SDJWTPresentationClaims import org.hyperledger.identus.walletsdk.domain.models.StorableCredential import org.hyperledger.identus.walletsdk.domain.models.UnknownError import org.hyperledger.identus.walletsdk.domain.models.httpClient @@ -85,7 +103,6 @@ import org.hyperledger.identus.walletsdk.domain.models.keyManagement.CurvePointY import org.hyperledger.identus.walletsdk.domain.models.keyManagement.ExportableKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.KeyTypes import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey -import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PublicKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.SignableKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.TypeKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.VerifiableKey @@ -95,9 +112,11 @@ import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.PresentationSubmissionOptions import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.PresentationSubmissionOptionsAnoncreds import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.PresentationSubmissionOptionsJWT +import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.PresentationSubmissionOptionsSDJWT +import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.SDJWTPresentationOptions +import org.hyperledger.identus.walletsdk.logger.LogComponent import org.hyperledger.identus.walletsdk.logger.Logger import org.hyperledger.identus.walletsdk.logger.LoggerImpl -import org.hyperledger.identus.walletsdk.logger.LogComponent import org.hyperledger.identus.walletsdk.pluto.RestorationID import org.hyperledger.identus.walletsdk.pollux.models.AnonCredential import org.hyperledger.identus.walletsdk.pollux.models.AnoncredsPresentationDefinitionRequest @@ -105,11 +124,18 @@ import org.hyperledger.identus.walletsdk.pollux.models.DescriptorItemFormat import org.hyperledger.identus.walletsdk.pollux.models.JWTCredential import org.hyperledger.identus.walletsdk.pollux.models.JWTPresentationDefinitionRequest import org.hyperledger.identus.walletsdk.pollux.models.JWTProofType +import org.hyperledger.identus.walletsdk.pollux.models.PresentationDefinition import org.hyperledger.identus.walletsdk.pollux.models.PresentationSubmission import org.hyperledger.identus.walletsdk.pollux.models.SDJWTCredential +import org.hyperledger.identus.walletsdk.pollux.models.SDJWTPresentationDefinitionRequest import org.hyperledger.identus.walletsdk.pollux.models.VerificationKeyType import org.hyperledger.identus.walletsdk.pollux.models.W3CCredential import org.hyperledger.identus.walletsdk.pollux.utils.BitString +import java.security.PublicKey +import java.security.Security +import java.security.spec.EdECPoint +import java.security.spec.EdECPublicKeySpec +import java.security.spec.X509EncodedKeySpec /** * Class representing the implementation of the Pollux interface. @@ -224,7 +250,7 @@ open class PolluxImpl( ): Credential { val cred: Credential when (restorationIdentifier) { - "sd-jwt+credential" -> { + RestorationID.SDJWT.value -> { cred = SDJWTCredential.fromSDJwtString(credentialData.decodeToString()) } @@ -285,7 +311,7 @@ open class PolluxImpl( ): String { val domain = getDomain(offerJson) ?: throw PolluxError.NoDomainOrChallengeFound() val challenge = getChallenge(offerJson) ?: throw PolluxError.NoDomainOrChallengeFound() - return signClaimsRequestCredentialJWT(subjectDID, privateKey, domain, challenge) + return signClaims(subjectDID, privateKey, domain, challenge) } /** @@ -312,6 +338,10 @@ open class PolluxImpl( (credential as AnonCredential).toStorableCredential() } + CredentialType.SDJWT -> { + (credential as SDJWTCredential).toStorableCredential() + } + else -> { throw PolluxError.InvalidCredentialError() } @@ -330,7 +360,8 @@ open class PolluxImpl( CredentialType.ANONCREDS_OFFER.type, CredentialType.ANONCREDS_REQUEST.type, CredentialType.ANONCREDS_ISSUE.type, - CredentialType.ANONCREDS_PROOF_REQUEST.type + CredentialType.ANONCREDS_PROOF_REQUEST.type, + CredentialType.SDJWT.type ) val foundFormat = formats.find { it.format in desiredFormats } return foundFormat?.format?.let { format -> @@ -340,6 +371,7 @@ open class PolluxImpl( CredentialType.ANONCREDS_REQUEST.type -> CredentialType.ANONCREDS_REQUEST CredentialType.ANONCREDS_ISSUE.type -> CredentialType.ANONCREDS_ISSUE CredentialType.ANONCREDS_PROOF_REQUEST.type -> CredentialType.ANONCREDS_PROOF_REQUEST + CredentialType.SDJWT.type -> CredentialType.SDJWT else -> throw Error("$format is not a valid credential type") } } ?: throw Error("Unknown credential type") @@ -592,20 +624,6 @@ open class PolluxImpl( throw UnknownError.SomethingWentWrongError("Fetch revocation registry failed: ${result.jsonString}") } - /** - * Parses a PrivateKey into an ECPrivateKey. - * - * @param privateKey The PrivateKey to parse. - * @return The parsed ECPrivateKey. - */ - internal fun parsePrivateKey(privateKey: PrivateKey): ECPrivateKey { - return privateKey.jca() as ECPrivateKey - } - - private fun parsePublicKey(publicKey: PublicKey): ECPublicKey { - return publicKey.jca() as ECPublicKey - } - /** * Returns the domain from the given JsonObject. * @@ -665,14 +683,14 @@ open class PolluxImpl( } /** - * Signs the claims for a proof presentation JSON Web Token (JWT). + * Signs the claims for a proof presentation JSON Web Token (SD+JWT). * - * @param subjectDID The DID of the subject for whom the JWT is being created. - * @param privateKey The private key used to sign the JWT. + * @param subjectDID The DID of the subject for whom the SD+JWT is being created. + * @param privateKey The private key used to sign the SD+JWT. * @param credential The credential to be included in the presentation. - * @param domain The domain of the JWT. - * @param challenge The challenge value for the JWT. - * @return The signed JWT as a string. + * @param domain The domain of the SD+JWT. + * @param challenge The challenge value for the SD+JWT. + * @return The signed SD+JWT as a string. */ internal suspend fun signClaimsProofPresentationSDJWT( subjectDID: DID, @@ -704,8 +722,6 @@ open class PolluxImpl( if (privateKey !is ExportableKey) { throw PolluxError.PrivateKeyTypeNotSupportedError("The private key should be ${ExportableKey::class.simpleName}") } - val ecPrivateKey = parsePrivateKey(privateKey) - val presentation: MutableMap> = mutableMapOf( CONTEXT to setOf(CONTEXT_URL), TYPE to setOf(VERIFIABLE_PRESENTATION) @@ -722,24 +738,62 @@ open class PolluxImpl( .build() val kid = getSigningKid(subjectDID) - - // Generate a JWS header with the ES256K algorithm - val header = JWSHeader.Builder(JWSAlgorithm.ES256K) - .keyID(kid) - .build() - + val algorithm = if (privateKey is Secp256k1PrivateKey) { + JWSAlgorithm.ES256K + } else { + JWSAlgorithm.EdDSA + } + val header = JWSHeader.Builder(algorithm).keyID(kid).build() // Sign the JWT with the private key - val jwsObject = SignedJWT(header, claims) - val signer = ECDSASigner( - ecPrivateKey as java.security.PrivateKey, - com.nimbusds.jose.jwk.Curve.SECP256K1 - ) - val provider = BouncyCastleProviderSingleton.getInstance() - signer.jcaContext.provider = provider - jwsObject.sign(signer) - - // Serialize the JWS object to a string - return jwsObject.serialize() + var jwsObject = SignedJWT(header, claims) + // Generate a JWS header with the ES256K algorithm + if (privateKey is Secp256k1PrivateKey) { + // Sign the JWT with the private key + val ecPrivateKey = privateKey.jca() as ECPrivateKey + val signer = ECDSASigner( + ecPrivateKey as java.security.PrivateKey, + com.nimbusds.jose.jwk.Curve.SECP256K1 + ) + val provider = BouncyCastleProviderSingleton.getInstance() + signer.jcaContext.provider = provider + jwsObject.sign(signer) + // Serialize the JWS object to a string + return jwsObject.serialize() + } else { + val edPrivateKey = privateKey.jca() as EdDSAPrivateKey + val signer = org.bouncycastle.crypto.signers.Ed25519Signer() + + // Convert the EdDSAPrivateKey to a Bouncy Castle private key parameter + val privateKeyParams = Ed25519PrivateKeyParameters(edPrivateKey.encoded, 0) + signer.init(true, privateKeyParams) + + // Sign the data (message) + val messageBytes = jwsObject.signingInput + signer.update(messageBytes, 0, messageBytes.size) + + // Generate the signature + val signature = signer.generateSignature() + + // Now, set the signature on the JWS object + jwsObject = SignedJWT( + header.toBase64URL(), + claims.toPayload().toBase64URL(), + Base64URL(signature.base64UrlEncoded)) + + // Serialize the JWS object to a string + val jwt = jwsObject.serialize() + val vc = JWTCredential.fromJwtString(jwt) + val didDocHolder = castor.resolveDID(vc.issuer) + val authenticationMethodHolder = + didDocHolder.coreProperties.find { it::class == DIDDocument.Authentication::class } + ?: throw PolluxError.VerificationUnsuccessful("Holder core properties must contain Authentication") + val pks = extractEdPublicKeyFromVerificationMethod(authenticationMethodHolder) + + if (!verifyJWTSignatureWithEdPublicKey( vc.id, pks ) ) { + throw PolluxError.VerificationUnsuccessful("Invalid JWT Signature") + } + return jwt + } } override suspend fun createPresentationDefinitionRequest( @@ -747,81 +801,151 @@ open class PolluxImpl( presentationClaims: PresentationClaims, options: PresentationOptions ): String { - val format: JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor.PresentationFormat - val inputDescriptor: JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor + val inputDescriptor: PresentationDefinition.InputDescriptor when (type) { CredentialType.JWT -> { - if (options !is JWTPresentationOptions) { - throw PolluxError.PresentationDefinitionRequestError("When type is ${type.type}, presentation options must be ${JWTPresentationOptions::class.simpleName}") - } - if (presentationClaims !is JWTPresentationClaims) { - throw PolluxError.PresentationDefinitionRequestError("When type is ${type.type}, presentation claims must be ${JWTPresentationClaims::class.simpleName}") + if (options.name == null || options.purpose == null) { + throw PolluxError.PresentationDefinitionRequestError("When type is ${type.type}, presentation options must contain name and purpose") } - val jwt = options.jwt - if (jwt.isEmpty()) { - throw PolluxError.InvalidJWTPresentationDefinitionError("Presentation option must contain at least one valid JWT alg that is not empty.") - } - val paths = presentationClaims.claims.keys - val mutableListFields: MutableList = - paths.map { path -> - JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor.Constraints.Field( - path = arrayOf("$.vc.credentialSubject.$path", "$.credentialSubject.$path"), - id = UUID.randomUUID().toString(), - optional = false, - filter = presentationClaims.claims[path], - name = path, + if (options is JWTPresentationOptions) { + if (presentationClaims !is JWTPresentationClaims) { + throw PolluxError.PresentationDefinitionRequestError("When type is ${type.type}, presentation claims must be ${JWTPresentationClaims::class.simpleName}") + } + + val paths = presentationClaims.claims.keys + val mutableListFields: MutableList = + paths.map { path -> + PresentationDefinition.InputDescriptor.Constraints.Field( + path = arrayOf("$.vc.credentialSubject.$path", "$.credentialSubject.$path", "$.$path"), + id = UUID.randomUUID().toString(), + optional = false, + filter = presentationClaims.claims[path], + name = path, + ) + } as MutableList + + presentationClaims.issuer?.let { issuer -> + mutableListFields.add( + PresentationDefinition.InputDescriptor.Constraints.Field( + path = arrayOf("$.issuer", "$.iss", "$.vc.iss", "$.vc.issuer"), + optional = false, + id = UUID.randomUUID().toString(), + filter = InputFieldFilter( + type = "String", + pattern = issuer + ), + name = "issuer" + ) + ) + } + + val constraints = + PresentationDefinition.InputDescriptor.Constraints( + fields = mutableListFields.toTypedArray(), + limitDisclosure = + PresentationDefinition.InputDescriptor.Constraints.LimitDisclosure.REQUIRED ) - } as MutableList - - presentationClaims.issuer?.let { issuer -> - mutableListFields.add( - JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor.Constraints.Field( - path = arrayOf("$.issuer", "$.iss", "$.vc.iss", "$.vc.issuer"), - optional = false, - id = UUID.randomUUID().toString(), - filter = InputFieldFilter( - type = "String", - pattern = issuer + + options as JWTPresentationOptions + val jwt = options.jwt + if (jwt.isEmpty()) { + throw PolluxError.InvalidJWTPresentationDefinitionError("Presentation option must contain at least one valid JWT alg that is not empty.") + } + val format = PresentationDefinition.InputDescriptor.PresentationFormat( + jwt = jwt.let { + PresentationDefinition.InputDescriptor.JwtFormat( + jwt.toList() + ) + } + ) + inputDescriptor = PresentationDefinition.InputDescriptor( + name = options.name, + purpose = options.purpose, + constraints = constraints, + format = format + ) + return Json.encodeToString( + JWTPresentationDefinitionRequest( + presentationDefinition = PresentationDefinition( + inputDescriptors = arrayOf(inputDescriptor), + format = format ), - name = "issuer" + options = JWTPresentationDefinitionRequest.PresentationDefinitionOptions( + domain = options.domain, + challenge = options.challenge + ) ) ) } + } - val constraints = JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor.Constraints( - fields = mutableListFields.toTypedArray(), - limitDisclosure = - JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor.Constraints.LimitDisclosure.REQUIRED - ) + CredentialType.SDJWT -> { + if (options.name == null || options.purpose == null) { + throw PolluxError.PresentationDefinitionRequestError("When type is ${type.type}, presentation options must contain name and purpose") + } + if (options is SDJWTPresentationOptions) { + if (presentationClaims !is SDJWTPresentationClaims) { + throw PolluxError.PresentationDefinitionRequestError("When type is ${type.type}, presentation claims must be ${SDJWTPresentationClaims::class.simpleName}") + } - format = - JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor.PresentationFormat( - jwt = jwt.let { - JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor.JwtFormat( - jwt.toList() + val paths = presentationClaims.claims.keys + val mutableListFields: MutableList = + paths.map { path -> + PresentationDefinition.InputDescriptor.Constraints.Field( + path = arrayOf("$.vc.credentialSubject.$path", "$.credentialSubject.$path", "$.$path"), + id = UUID.randomUUID().toString(), + optional = false, + filter = presentationClaims.claims[path], + name = path, + ) + } as MutableList + + presentationClaims.issuer?.let { issuer -> + mutableListFields.add( + PresentationDefinition.InputDescriptor.Constraints.Field( + path = arrayOf("$.issuer", "$.iss", "$.vc.iss", "$.vc.issuer"), + optional = false, + id = UUID.randomUUID().toString(), + filter = InputFieldFilter( + type = "String", + pattern = issuer + ), + name = "issuer" + ) + ) + } + + val constraints = + PresentationDefinition.InputDescriptor.Constraints( + fields = mutableListFields.toTypedArray(), + limitDisclosure = + PresentationDefinition.InputDescriptor.Constraints.LimitDisclosure.REQUIRED + ) + options as SDJWTPresentationOptions + val sdjwt = options.sdjwt + val format = PresentationDefinition.InputDescriptor.PresentationFormat( + sdjwt = sdjwt.let { + PresentationDefinition.InputDescriptor.JwtFormat( + sdjwt.toList() ) } ) + inputDescriptor = PresentationDefinition.InputDescriptor( + name = options.name, + purpose = options.purpose, + constraints = constraints, + format = format - inputDescriptor = JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor( - name = options.name, - purpose = options.purpose, - constraints = constraints, - format = format - ) - - return Json.encodeToString( - JWTPresentationDefinitionRequest( - presentationDefinition = JWTPresentationDefinitionRequest.PresentationDefinition( - inputDescriptors = arrayOf(inputDescriptor), - format = format - ), - options = JWTPresentationDefinitionRequest.PresentationDefinitionOptions( - domain = options.domain, - challenge = options.challenge + ) + return Json.encodeToString( + SDJWTPresentationDefinitionRequest( + presentationDefinition = PresentationDefinition( + inputDescriptors = arrayOf(inputDescriptor), + format = format + ) ) ) - ) + } } CredentialType.ANONCREDS_PROOF_REQUEST -> { @@ -847,13 +971,15 @@ open class PolluxImpl( } else if (inputFilter.lte != null) { "<=" } else { - throw Exception() // TODO: Custom exception + throw PolluxError.PresentationDefinitionRequestError("Input filter must contain at one of the following gt, gte, lt, lte as not null") } - val pValue = (inputFilter.gt - ?: (inputFilter.gte - ?: (inputFilter.lt - ?: (inputFilter.lte ?: throw Exception())))) as Int // TODO: Custom excetion + val pValue = + (inputFilter.gt + ?: (inputFilter.gte + ?: (inputFilter.lt + ?: (inputFilter.lte + ?: throw PolluxError.PresentationDefinitionRequestError("Input filter must contain at one of the following gt, gte, lt, lte as not null"))))) as Int // Based on the definition of AnoncredsPresentationClaims we do not need to verify if key is duplicated. mapPredicate[key] = RequestedPredicates( @@ -879,12 +1005,21 @@ open class PolluxImpl( else -> { throw PolluxError.CredentialTypeNotSupportedError( "Credential type ${type.type} not supported. " + - "Must be ${CredentialType.JWT.type} or ${CredentialType.ANONCREDS_PROOF_REQUEST.type}" + "Must be ${CredentialType.JWT.type}, ${CredentialType.SDJWT.type} or ${CredentialType.ANONCREDS_PROOF_REQUEST.type}" ) } } + throw PolluxError.CredentialTypeNotSupportedError( + "Credential type ${type.type} not supported. " + + "Must be ${CredentialType.JWT.type}, ${CredentialType.SDJWT.type} or ${CredentialType.ANONCREDS_PROOF_REQUEST.type}" + ) } + @Deprecated( + "This method is deprecated, this functionality was moved to each credential", + ReplaceWith("ProvableCredential.presentation(attachmentFormat, request, options)"), + DeprecationLevel.ERROR + ) override suspend fun createJWTPresentationSubmission( presentationDefinitionRequest: String, credential: Credential, @@ -949,6 +1084,11 @@ open class PolluxImpl( } ?: throw PolluxError.NonNullableError("CredentialSubject") } + @Deprecated( + "This method is deprecated, this functionality was moved to each credential", + ReplaceWith("ProvableCredential.presentation(attachmentFormat, request, options)"), + DeprecationLevel.ERROR + ) override suspend fun createAnoncredsPresentationSubmission( presentationDefinitionRequest: String, credential: Credential, @@ -1027,228 +1167,325 @@ open class PolluxImpl( presentationSubmissionString: String, options: PresentationSubmissionOptions ): Boolean { - if (options::class == PresentationSubmissionOptionsJWT::class) { - val presentationSubmissionJsonObject = - Json.decodeFromString(presentationSubmissionString).jsonObject - val presentationSubmission = - presentationSubmissionJsonObject["presentation_submission"]?.let { presentationSubmissionField -> - val submission = - Json.decodeFromJsonElement( - presentationSubmissionField - ) - var arrayStrings: Array = arrayOf() - - if (submission.descriptorMap.isNotEmpty()) { - val firstDescriptorItem = submission.descriptorMap.first() - // Assume the path denotes a direct key in the JSON and strip out JSONPath or XPath specific characters if any. - val path = firstDescriptorItem.path.removePrefix("$.") - .removeSuffix("[0]") // Adjust based on actual path format - arrayStrings = - presentationSubmissionJsonObject[path]?.jsonArray?.map { it.jsonPrimitive.content } - ?.toTypedArray() - ?: arrayOf() - } - return@let PresentationSubmission(submission, arrayStrings) - } ?: throw PolluxError.VerificationUnsuccessful("Presentation is missing presentation_submission") - -// val presentationDefinitionRequest = -// (options as PresentationSubmissionOptionsJWT).presentationDefinitionRequest as JWTPresentationDefinitionRequest -// presentationDefinitionRequest.presentationDefinition -// val inputDescriptors = -// presentationDefinitionRequest.presentationDefinition.inputDescriptors - val presentationDefinitionRequestString = - (options as PresentationSubmissionOptionsJWT).presentationDefinitionRequest - val presentationDefinitionRequest = - Json.decodeFromString(presentationDefinitionRequestString) - val inputDescriptors = presentationDefinitionRequest.presentationDefinition.inputDescriptors - val descriptorMap = DescriptorPath(Json.encodeToJsonElement(presentationSubmission)) - val descriptorMaps = presentationSubmission.presentationSubmission.descriptorMap - descriptorMaps.forEach { descriptorItem -> - if (descriptorItem.format != DescriptorItemFormat.JWT_VP.value) { - throw PolluxError.VerificationUnsuccessful("Invalid submission, ${descriptorItem.path} expected to have format ${DescriptorItemFormat.JWT_VP.value}") + return when (options) { + is PresentationSubmissionOptionsJWT -> { + verifyJWTPresentation(presentationSubmissionString, options) + } + + is PresentationSubmissionOptionsSDJWT -> { + verifySDJWTPresentation(presentationSubmissionString, options) + } + + is PresentationSubmissionOptionsAnoncreds -> { + verifyAnoncredsPresentation(presentationSubmissionString, options) + } + + else -> return false + } + } + + private suspend fun verifyJWTPresentation( + presentationSubmissionString: String, + options: PresentationSubmissionOptionsJWT + ): Boolean { + val presentationSubmissionJsonObject = + Json.decodeFromString(presentationSubmissionString).jsonObject + val presentationSubmission = + presentationSubmissionJsonObject["presentation_submission"]?.let { presentationSubmissionField -> + val submission = + Json.decodeFromJsonElement( + presentationSubmissionField + ) + var arrayStrings: Array = arrayOf() + + if (submission.descriptorMap.isNotEmpty()) { + val firstDescriptorItem = submission.descriptorMap.first() + // Assume the path denotes a direct key in the JSON and strip out JSONPath or XPath specific characters if any. + val path = firstDescriptorItem.path.removePrefix("$.") + .removeSuffix("[0]") // Adjust based on actual path format + arrayStrings = + presentationSubmissionJsonObject[path]?.jsonArray?.map { it.jsonPrimitive.content } + ?.toTypedArray() + ?: arrayOf() } + PresentationSubmission(submission, arrayStrings) + } ?: throw PolluxError.VerificationUnsuccessful("Presentation is missing presentation_submission") + + val presentationDefinitionRequestString = options.presentationDefinitionRequest + val presentationDefinitionRequest = + Json.decodeFromString(presentationDefinitionRequestString) + + val inputDescriptors = presentationDefinitionRequest.presentationDefinition.inputDescriptors + val descriptorMap = DescriptorPath(Json.encodeToJsonElement(presentationSubmission)) + val descriptorMaps = presentationSubmission.presentationSubmission.descriptorMap + descriptorMaps.forEach { descriptorItem -> + if (descriptorItem.format != DescriptorItemFormat.JWT_VP.value) { + throw PolluxError.VerificationUnsuccessful("Invalid submission, ${descriptorItem.path} expected to have format ${DescriptorItemFormat.JWT_VP.value}") + } - var newPath: String? = null - if (!descriptorItem.path.contains("verifiablePresentation")) { - newPath = - PresentationSubmission.Submission.DescriptorItem.replacePathWithVerifiablePresentation( - descriptorItem.path - ) + var newPath: String? = null + if (!descriptorItem.path.contains("verifiablePresentation")) { + newPath = + PresentationSubmission.Submission.DescriptorItem.replacePathWithVerifiablePresentation( + descriptorItem.path + ) + } + val jws = + descriptorMap.getValue(newPath ?: descriptorItem.path) + ?: throw PolluxError.VerificationUnsuccessful("Could not find ${descriptorItem.path} value") + val presentation = JWTCredential.fromJwtString(jws as String) + val issuer = presentation.issuer + + val presentationDefinitionOptions = presentationDefinitionRequest.options + val challenge = presentationDefinitionOptions.challenge + if (challenge.isNotBlank()) { + val nonce = presentation.nonce + if (nonce.isNullOrBlank()) { + throw PolluxError.VerificationUnsuccessful("Invalid submission, ${descriptorItem.path} does snot contain a nonce with a valid signature for the challenge.") + } + if (challenge != nonce) { + throw PolluxError.VerificationUnsuccessful("Invalid submission, the signature from ${descriptorItem.path} is not valid for the challenge.") } - val jws = - descriptorMap.getValue(newPath ?: descriptorItem.path) - ?: throw PolluxError.VerificationUnsuccessful("Could not find ${descriptorItem.path} value") - val presentation = JWTCredential.fromJwtString(jws as String) - val issuer = presentation.issuer - - val presentationDefinitionOptions = presentationDefinitionRequest.options - val challenge = presentationDefinitionOptions.challenge - if (challenge.isNotBlank()) { - val nonce = presentation.nonce - if (nonce.isNullOrBlank()) { - throw PolluxError.VerificationUnsuccessful("Invalid submission, ${descriptorItem.path} does snot contain a nonce with a valid signature for the challenge.") + } + + descriptorItem.pathNested?.let { pathNested -> + val verifiableCredentialMapper = + DescriptorPath(Json.encodeToJsonElement(presentation)) + val value = verifiableCredentialMapper.getValue(pathNested.path) + value?.let { vc -> + val verifiableCredential = JWTCredential.fromJwtString(vc as String) + + val isRevoked = isCredentialRevoked(verifiableCredential) + + if (isRevoked) { + throw PolluxError.VerificationUnsuccessful("Provided credential is revoked") } - if (challenge != nonce) { - throw PolluxError.VerificationUnsuccessful("Invalid submission, the signature from ${descriptorItem.path} is not valid for the challenge.") + + if (verifiableCredential.subject != issuer) { + throw PolluxError.VerificationUnsuccessful("Invalid submission,") } + + val didDocHolder = castor.resolveDID(verifiableCredential.issuer) + val authenticationMethodHolder = + didDocHolder.coreProperties.find { it::class == DIDDocument.Authentication::class } + ?: throw PolluxError.VerificationUnsuccessful("Holder core properties must contain Authentication") + val ecPublicKeysHolder = + extractEcPublicKeyFromVerificationMethod(authenticationMethodHolder) + + if (!verifyJWTSignatureWithEcPublicKey( + verifiableCredential.id, + ecPublicKeysHolder + ) + ) { + throw PolluxError.VerificationUnsuccessful("Invalid presentation credential JWT Signature") + } + + // Now we are going to validate the requested fields with the provided credentials + val inputDescriptor = + inputDescriptors.find { it.id == descriptorItem.id } + verifyInputDescriptors(inputDescriptor, Json.encodeToJsonElement(verifiableCredential)) + } ?: throw PolluxError.VerificationUnsuccessful("Invalid submission, no value found for $pathNested") + return true + } + } + return false + } + + private suspend fun verifySDJWTPresentation( + presentationSubmissionString: String, + options: PresentationSubmissionOptionsSDJWT + ): Boolean { + val presentationSubmissionJsonObject = + Json.decodeFromString(presentationSubmissionString).jsonObject + val presentationSubmission = + presentationSubmissionJsonObject["presentation_submission"]?.let { presentationSubmissionField -> + val submission = + Json.decodeFromJsonElement( + presentationSubmissionField + ) + var arrayStrings: Array = arrayOf() + + if (submission.descriptorMap.isNotEmpty()) { + val firstDescriptorItem = submission.descriptorMap.first() + // Assume the path denotes a direct key in the JSON and strip out JSONPath or XPath specific characters if any. + val path = firstDescriptorItem.path.removePrefix("$.") + .removeSuffix("[0]") // Adjust based on actual path format + arrayStrings = + presentationSubmissionJsonObject[path]?.jsonArray?.map { it.jsonPrimitive.content } + ?.toTypedArray() + ?: arrayOf() } + PresentationSubmission(submission, arrayStrings) + } ?: throw PolluxError.VerificationUnsuccessful("Presentation is missing presentation_submission") - descriptorItem.pathNested?.let { pathNested -> - val verifiableCredentialMapper = - DescriptorPath(Json.encodeToJsonElement(presentation)) - val value = verifiableCredentialMapper.getValue(pathNested.path) - value?.let { vc -> - val verifiableCredential = JWTCredential.fromJwtString(vc as String) + val presentationDefinitionRequestString = + options.presentationDefinitionRequest + val presentationDefinitionRequest = + Json.decodeFromString(presentationDefinitionRequestString) - val isRevoked = isCredentialRevoked(verifiableCredential) + val descriptorMap = DescriptorPath(Json.encodeToJsonElement(presentationSubmission)) - if (isRevoked) { - throw PolluxError.VerificationUnsuccessful("Provided credential is revoked") - } + val descriptorMaps = presentationSubmission.presentationSubmission.descriptorMap + descriptorMaps.forEach { descriptorItem -> + if (descriptorItem.format != DescriptorItemFormat.SD_JWT_VP.value) { + throw PolluxError.VerificationUnsuccessful("Invalid submission, ${descriptorItem.path} expected to have format ${DescriptorItemFormat.JWT_VP.value}") + } - if (verifiableCredential.subject != issuer) { - throw PolluxError.VerificationUnsuccessful("Invalid submission,") - } + var newPath: String? = null + if (!descriptorItem.path.contains("verifiablePresentation")) { + newPath = + PresentationSubmission.Submission.DescriptorItem.replacePathWithVerifiablePresentation( + descriptorItem.path + ) + } + val jws = + descriptorMap.getValue(newPath ?: descriptorItem.path) + ?: throw PolluxError.VerificationUnsuccessful("Could not find ${descriptorItem.path} value") + + val verification = SdJwtVerifier.verifyPresentation( + JwtSignatureVerifier.NoSignatureValidation, + KeyBindingVerifier.MustNotBePresent, + jws as String + ) + if (verification.isFailure) { + throw PolluxError.VerificationUnsuccessful("Invalid SD-JWT presentation") + } - val didDocHolder = castor.resolveDID(verifiableCredential.issuer) - val authenticationMethodHolder = - didDocHolder.coreProperties.find { it::class == DIDDocument.Authentication::class } - ?: throw PolluxError.VerificationUnsuccessful("Holder core properties must contain Authentication") - val ecPublicKeysHolder = - extractEcPublicKeyFromVerificationMethod(authenticationMethodHolder) + val presentation = SDJWTCredential.fromSDJwtString(jws) - if (!verifyJWTSignatureWithEcPublicKey( - verifiableCredential.id, - ecPublicKeysHolder - ) - ) { - throw PolluxError.VerificationUnsuccessful("Invalid presentation credential JWT Signature") - } + val claimsAndDisclosures = presentation.sdjwt.recreateClaimsAndDisclosuresPerClaim { it.second } + val inputDescriptors = presentationDefinitionRequest.presentationDefinition.inputDescriptors - // Now we are going to validate the requested fields with the provided credentials - val verifiableCredentialDescriptorPath = - DescriptorPath(Json.encodeToJsonElement(verifiableCredential)) - val inputDescriptor = - inputDescriptors.find { it.id == descriptorItem.id } - if (inputDescriptor != null) { - val constraints = inputDescriptor.constraints - val fields = constraints.fields - if (constraints.limitDisclosure == JWTPresentationDefinitionRequest.PresentationDefinition.InputDescriptor.Constraints.LimitDisclosure.REQUIRED) { - fields?.forEach { field -> - val optional = field.optional - if (!optional) { - var validClaim = false - var reason = "" - val paths = field.path - paths.forEach { path -> - val fieldValue = - verifiableCredentialDescriptorPath.getValue(path) - if (fieldValue != null) { - if (field.filter != null) { - val filter: InputFieldFilter = field.filter - filter.pattern?.let { pattern -> - val regexPattern = Regex(pattern) - if (regexPattern.matches(fieldValue.toString()) || fieldValue == pattern) { - validClaim = true - return@forEach - } else { - reason = - "Expected the $path field to be $pattern but got $fieldValue" - } - } - filter.enum?.let { enum -> - enum.forEach { predicate -> - if (fieldValue == predicate) { - validClaim = true - return@forEach - } - } - if (!validClaim) { - reason = - "Expected the $path field to be one of ${filter.enum.joinToString { ", " }} but got $fieldValue" - } - } - filter.const?.let { const -> - const.forEach { constValue -> - if (fieldValue == constValue) { - validClaim = true - return@forEach - } - } - if (!validClaim) { - reason = - "Expected the $path field to be one of ${filter.const.joinToString { ", " }} but got $fieldValue" - } - } - filter.value?.let { value -> - if (value == fieldValue) { - validClaim = true - return@forEach - } else { - reason = - "Expected the $path field to be $value but got $fieldValue" - } - } - } else { - reason = - "Input field filter for ${field.name} is null" - } - } else { - reason = "Field value for path $path is null" + val inputDescriptor = + inputDescriptors.find { it.id == descriptorItem.id } + verifyInputDescriptors(inputDescriptor, claimsAndDisclosures.first.toJsonElement()) + return true + } + return false + } + + private fun verifyInputDescriptors( + inputDescriptor: PresentationDefinition.InputDescriptor?, + descriptorPath: JsonElement + ) { + val claimsAndDisclosures = DescriptorPath(descriptorPath) + if (inputDescriptor != null) { + val constraints = inputDescriptor.constraints + val fields = constraints.fields + if (constraints.limitDisclosure == PresentationDefinition.InputDescriptor.Constraints.LimitDisclosure.REQUIRED) { + fields?.forEach { field -> + val optional = field.optional + if (!optional) { + var validClaim = false + var reason = "" + val paths = field.path + paths.forEach { path -> + val fieldValue = + claimsAndDisclosures.getValue(path) + if (fieldValue != null) { + if (field.filter != null) { + val filter: InputFieldFilter = field.filter + filter.pattern?.let { pattern -> + val regexPattern = Regex(pattern) + if (regexPattern.matches(fieldValue.toString()) || fieldValue == pattern) { + validClaim = true + return@forEach + } else { + reason = + "Expected the $path field to be $pattern but got $fieldValue" + } + } + filter.enum?.let { enum -> + enum.forEach { predicate -> + if (fieldValue == predicate) { + validClaim = true + return@forEach + } + } + if (!validClaim) { + reason = + "Expected the $path field to be one of ${filter.enum.joinToString { ", " }} but got $fieldValue" + } + } + filter.const?.let { const -> + const.forEach { constValue -> + if (fieldValue == constValue) { + validClaim = true + return@forEach } } if (!validClaim) { - throw PolluxError.VerificationUnsuccessful(reason) + reason = + "Expected the $path field to be one of ${filter.const.joinToString { ", " }} but got $fieldValue" + } + } + filter.value?.let { value -> + if (value == fieldValue) { + validClaim = true + return@forEach + } else { + reason = + "Expected the $path field to be $value but got $fieldValue" } } + } else { + reason = + "Input field filter for ${field.name} is null" } + } else { + reason = "Field value for path $path is null" } } + if (!validClaim) { + throw PolluxError.VerificationUnsuccessful(reason) + } } - ?: throw PolluxError.VerificationUnsuccessful("Invalid submission, no value found for $pathNested") - return true } } - } else if (options::class == PresentationSubmissionOptionsAnoncreds::class) { - val presentationDefinitionRequest = - (options as PresentationSubmissionOptionsAnoncreds).presentationDefinitionRequest - val anoncredsPresentationDefinitionRequest = - Json.decodeFromString(presentationDefinitionRequest) - - val presentationRequest = PresentationRequest(Json.encodeToString(anoncredsPresentationDefinitionRequest)) - val presentation = Presentation(presentationSubmissionString) - val presentationJsonObject = Json.parseToJsonElement(presentationSubmissionString).jsonObject - if (!presentationJsonObject.containsKey("identifiers")) { - throw Exception() - } - val identifiers = presentationJsonObject["identifiers"]!!.jsonArray - val identifier = identifiers.first().jsonObject - if (!identifier.containsKey("schema_id")) { - throw Exception() - } - val schemaId = identifier["schema_id"]!!.jsonPrimitive.content - val schema = getSchema(schemaId) + } + } - if (!identifier.containsKey("cred_def_id")) { - throw Exception() - } - val credDefId = identifier["cred_def_id"]!!.jsonPrimitive.content - val credentialDefinition = getCredentialDefinition(credDefId) - - val schemaMap: Map = mapOf(Pair(schemaId, schema)) - val credDefinition: Map = mapOf(Pair(credDefId, credentialDefinition)) - - return Verifier().verifyPresentation( - presentation = presentation, - presReq = presentationRequest, - schemas = schemaMap, - credDefs = credDefinition, - revRegDefs = null, - revStatusLists = null, - nonrevokeIntervalOverride = null - ) + private suspend fun verifyAnoncredsPresentation( + presentationSubmissionString: String, + options: PresentationSubmissionOptionsAnoncreds + ): Boolean { + val presentationDefinitionRequest = + (options as PresentationSubmissionOptionsAnoncreds).presentationDefinitionRequest + val anoncredsPresentationDefinitionRequest = + Json.decodeFromString(presentationDefinitionRequest) + + val presentationRequest = PresentationRequest(Json.encodeToString(anoncredsPresentationDefinitionRequest)) + val presentation = Presentation(presentationSubmissionString) + val presentationJsonObject = Json.parseToJsonElement(presentationSubmissionString).jsonObject + if (!presentationJsonObject.containsKey("identifiers")) { + throw Exception() } - return false + val identifiers = presentationJsonObject["identifiers"]!!.jsonArray + val identifier = identifiers.first().jsonObject + if (!identifier.containsKey("schema_id")) { + throw Exception() + } + val schemaId = identifier["schema_id"]!!.jsonPrimitive.content + val schema = getSchema(schemaId) + + if (!identifier.containsKey("cred_def_id")) { + throw Exception() + } + val credDefId = identifier["cred_def_id"]!!.jsonPrimitive.content + val credentialDefinition = getCredentialDefinition(credDefId) + + val schemaMap: Map = mapOf(Pair(schemaId, schema)) + val credDefinition: Map = mapOf(Pair(credDefId, credentialDefinition)) + + return Verifier().verifyPresentation( + presentation = presentation, + presReq = presentationRequest, + schemas = schemaMap, + credDefs = credDefinition, + revRegDefs = null, + revStatusLists = null, + nonrevokeIntervalOverride = null + ) } internal fun verifyJWTSignatureWithEcPublicKey( @@ -1275,6 +1512,44 @@ open class PolluxImpl( return areVerified.find { it } ?: false } + internal fun verifyJWTSignatureWithEdPublicKey( + jwtString: String, + publicKeys: Array + ): Boolean { + Security.addProvider(BouncyCastleProvider()) + + val jwtPartsIssuer = jwtString.split(".") + if (jwtPartsIssuer.size != 3) { + throw PolluxError.InvalidJWTString("Invalid JWT string, must contain 3 parts.") + } + val jwsObject = + SignedJWT( + Base64URL(jwtPartsIssuer[0]), + Base64URL(jwtPartsIssuer[1]), + Base64URL(jwtPartsIssuer[2]) + ) + val areVerified = publicKeys.map { pk -> + + val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(pk.encoded) + val rawPublicKeyBytes: ByteArray = subjectPublicKeyInfo.publicKeyData.bytes + val octet = OctetKeyPair + .Builder( + com.nimbusds.jose.jwk.Curve.Ed25519, + Base64URL.encode(rawPublicKeyBytes) + ) + .keyUse(KeyUse.SIGNATURE) + .build() + + + + val verifiers = Ed25519Verifier(octet) + val provider = BouncyCastleProviderSingleton.getInstance() + verifiers.jcaContext.provider = provider + jwsObject.verify(verifiers) + } + return areVerified.find { it } ?: false + } + override suspend fun extractEcPublicKeyFromVerificationMethod(coreProperty: DIDDocumentCoreProperty): Array { val publicKeys = castor.getPublicKeysFromCoreProperties(arrayOf(coreProperty)) @@ -1303,6 +1578,43 @@ open class PolluxImpl( return ecPublicKeys.toTypedArray() } + private suspend fun extractEdPublicKeyFromVerificationMethod(coreProperty: DIDDocumentCoreProperty): Array { + val publicKeys = castor.getPublicKeysFromCoreProperties(arrayOf(coreProperty)) + .filter { + it.getCurve().lowercase() == Curve.ED25519.toString().lowercase() + } + + val edPublicKeys = publicKeys.map { publicKey -> + when (DIDDocument.VerificationMethod.getCurveByType(publicKey.getCurve())) { + Curve.ED25519 -> { + // Add BouncyCastle as a security provider if not already added + if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { + Security.addProvider(BouncyCastleProvider()) + } + + // Create AlgorithmIdentifier for Ed25519 + val algId = AlgorithmIdentifier(EdECObjectIdentifiers.id_Ed25519) + + // Create SubjectPublicKeyInfo with AlgorithmIdentifier and public key bytes + val subjectPublicKeyInfo = SubjectPublicKeyInfo(algId, publicKey.raw) + + // Get the encoded bytes for the X509EncodedKeySpec + val x509Bytes = subjectPublicKeyInfo.getEncoded(ASN1Encoding.DER) + + // Generate EdDSAPublicKey from x509Bytes + val keyFactory = KeyFactory.getInstance("Ed25519", BouncyCastleProvider.PROVIDER_NAME) + val x509KeySpec = X509EncodedKeySpec(x509Bytes) + keyFactory.generatePublic(x509KeySpec) as EdDSAPublicKey + } + else -> { + throw Exception("Key type not supported ${publicKey.getCurve()}") + } + } + } + + return edPublicKeys.toTypedArray() + } + /** * Method to get the kId from the DID authentication property, Master key. * diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/AnonCredential.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/AnonCredential.kt index 8e2520dcf..16407f2fe 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/AnonCredential.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/AnonCredential.kt @@ -143,7 +143,7 @@ data class AnonCredential( override var revoked: Boolean? = null - override suspend fun presentation(request: ByteArray, options: List): String { + override suspend fun presentation(attachmentFormat: String, request: ByteArray, options: List): String { var schemaDownloader: Api? = null var definitionDownloader: Api? = null var linkSecret: String? = null diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/JWTCredential.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/JWTCredential.kt index 52b857ba6..63f41f1da 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/JWTCredential.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/JWTCredential.kt @@ -14,6 +14,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.Transient import kotlinx.serialization.builtins.ArraySerializer import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement @@ -29,6 +30,7 @@ import org.hyperledger.identus.walletsdk.domain.models.Claim import org.hyperledger.identus.walletsdk.domain.models.ClaimType import org.hyperledger.identus.walletsdk.domain.models.Credential import org.hyperledger.identus.walletsdk.domain.models.CredentialOperationsOptions +import org.hyperledger.identus.walletsdk.domain.models.CredentialType import org.hyperledger.identus.walletsdk.domain.models.DID import org.hyperledger.identus.walletsdk.domain.models.JWTPayload import org.hyperledger.identus.walletsdk.domain.models.JWTVerifiableCredential @@ -36,6 +38,8 @@ import org.hyperledger.identus.walletsdk.domain.models.JWTVerifiablePresentation import org.hyperledger.identus.walletsdk.domain.models.PolluxError import org.hyperledger.identus.walletsdk.domain.models.ProvableCredential import org.hyperledger.identus.walletsdk.domain.models.StorableCredential +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey +import org.hyperledger.identus.walletsdk.domain.models.keyManagement.SignableKey import org.hyperledger.identus.walletsdk.pollux.CHALLENGE import org.hyperledger.identus.walletsdk.pollux.CONTEXT import org.hyperledger.identus.walletsdk.pollux.CONTEXT_URL @@ -46,8 +50,9 @@ import org.hyperledger.identus.walletsdk.pollux.TYPE import org.hyperledger.identus.walletsdk.pollux.VERIFIABLE_CREDENTIAL import org.hyperledger.identus.walletsdk.pollux.VERIFIABLE_PRESENTATION import org.hyperledger.identus.walletsdk.pollux.VP -import java.security.PrivateKey import java.security.interfaces.ECPrivateKey +import java.util.* +import java.security.PrivateKey as JavaPrivateKey @Serializable /** @@ -126,23 +131,92 @@ data class JWTCredential @JvmOverloads constructor( } override var revoked: Boolean? = false - override suspend fun presentation(request: ByteArray, options: List): String { - var exportableKeyOption: org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey? = null + + override suspend fun presentation( + attachmentFormat: String, + request: ByteArray, + options: List + ): String { + var exportableKey: PrivateKey? = null var subjectDID: DID? = null for (option in options) { when (option) { is CredentialOperationsOptions.SubjectDID -> subjectDID = option.did - is CredentialOperationsOptions.ExportableKey -> exportableKeyOption = option.key + is CredentialOperationsOptions.ExportableKey -> exportableKey = option.key else -> {} } } if (subjectDID == null) { throw PolluxError.InvalidPrismDID() } - if (exportableKeyOption == null) { + if (exportableKey == null) { throw PolluxError.WrongKeyProvided("Secp256k1", actual = "null") } + + return when (attachmentFormat) { + CredentialType.PRESENTATION_EXCHANGE_DEFINITIONS.type -> { + presentationSubmission(request, exportableKey) + } + + else -> { + vcPresentation(request, subjectDID, exportableKey) + } + } + } + + private fun presentationSubmission(request: ByteArray, privateKey: PrivateKey): String { + val jwtPresentationDefinitionRequest = + Json.decodeFromString(String(request, Charsets.UTF_8)) + val descriptorItems = + jwtPresentationDefinitionRequest.presentationDefinition.inputDescriptors.map { inputDescriptor -> + if (inputDescriptor.format != null && (inputDescriptor.format.jwt == null || inputDescriptor.format.jwt.alg.isEmpty())) { + throw PolluxError.InvalidCredentialDefinitionError() + } + PresentationSubmission.Submission.DescriptorItem( + id = inputDescriptor.id, + format = DescriptorItemFormat.JWT_VP.value, + path = "$.verifiablePresentation[0]", + pathNested = PresentationSubmission.Submission.DescriptorItem( + id = inputDescriptor.id, + format = DescriptorItemFormat.JWT_VC.value, + path = "$.vp.verifiableCredential[0]" + ) + ) + }.toTypedArray() + + val credentialSubject = this.subject + credentialSubject?.let { subject -> + if (!privateKey.isSignable()) { + throw PolluxError.WrongKeyProvided( + expected = SignableKey::class.simpleName, + actual = privateKey::class.simpleName + ) + } + + val ecPrivateKey = privateKey.jca() as ECPrivateKey + val presentationJwt = signClaimsProofPresentationJWT( + subjectDID = DID(subject), + privateKey = ecPrivateKey, + credential = this, + domain = jwtPresentationDefinitionRequest.options.domain, + challenge = jwtPresentationDefinitionRequest.options.challenge + ) + + return Json.encodeToString( + PresentationSubmission( + presentationSubmission = PresentationSubmission.Submission( + definitionId = jwtPresentationDefinitionRequest.presentationDefinition.id + ?: UUID.randomUUID().toString(), + descriptorMap = descriptorItems + ), + verifiablePresentation = arrayOf(presentationJwt) + ) + ) + } ?: throw PolluxError.NonNullableError("CredentialSubject") + } + + private fun vcPresentation(request: ByteArray, subjectDID: DID, exportableKey: PrivateKey): String { val jsonString = String(request, Charsets.UTF_8) val requestJson = Json.parseToJsonElement(jsonString).jsonObject val domain = @@ -151,7 +225,7 @@ data class JWTCredential @JvmOverloads constructor( getChallenge(requestJson) ?: throw PolluxError.NoDomainOrChallengeFound() return signClaimsProofPresentationJWT( subjectDID, - exportableKeyOption.jca() as ECPrivateKey, + exportableKey.jca() as ECPrivateKey, this, domain, challenge @@ -319,7 +393,7 @@ data class JWTCredential @JvmOverloads constructor( // Sign the JWT with the private key val jwsObject = SignedJWT(header, claims) val signer = ECDSASigner( - privateKey as PrivateKey, + privateKey as JavaPrivateKey, Curve.SECP256K1 ) val provider = BouncyCastleProviderSingleton.getInstance() diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/PresentationDefinitionRequest.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/PresentationDefinitionRequest.kt index 970d7c477..322c3db9e 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/PresentationDefinitionRequest.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/PresentationDefinitionRequest.kt @@ -40,108 +40,50 @@ data class JWTPresentationDefinitionRequest( val domain: String, val challenge: String ) +} + +@Serializable +data class SDJWTPresentationDefinitionRequest( + @SerialName("presentation_definition") + val presentationDefinition: PresentationDefinition +) : PresentationDefinitionRequest +@Serializable +data class PresentationDefinition( + val id: String? = UUID.randomUUID().toString(), + @SerialName("input_descriptors") + val inputDescriptors: Array, + val format: InputDescriptor.PresentationFormat +) { @Serializable - data class PresentationDefinition( - val id: String? = UUID.randomUUID().toString(), - @SerialName("input_descriptors") - val inputDescriptors: Array, - val format: InputDescriptor.PresentationFormat + data class InputDescriptor( + val id: String = UUID.randomUUID().toString(), + val name: String? = null, + val purpose: String? = null, + val format: PresentationFormat? = null, + val constraints: Constraints ) { + @Serializable - data class InputDescriptor( - val id: String = UUID.randomUUID().toString(), - val name: String? = null, - val purpose: String? = null, - val format: PresentationFormat? = null, - val constraints: Constraints + data class Constraints @JvmOverloads constructor( + val fields: Array? = null, + @SerialName("limit_disclosure") + val limitDisclosure: LimitDisclosure? = null ) { - @Serializable - data class Constraints @JvmOverloads constructor( - val fields: Array? = null, - @SerialName("limit_disclosure") - val limitDisclosure: LimitDisclosure? = null + data class Field @JvmOverloads constructor( + val path: Array, + val id: String? = null, + val purpose: String? = null, + val name: String? = null, + val filter: InputFieldFilter? = null, + val optional: Boolean = false ) { @Serializable - data class Field @JvmOverloads constructor( - val path: Array, - val id: String? = null, - val purpose: String? = null, - val name: String? = null, - val filter: InputFieldFilter? = null, - val optional: Boolean = false - ) { - @Serializable - data class Filter( - val type: String, - val pattern: String - ) - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - if (javaClass != other?.javaClass) { - return false - } - - other as Field - - if (!path.contentEquals(other.path)) { - return false - } - if (id != other.id) { - return false - } - if (purpose != other.purpose) { - return false - } - if (name != other.name) { - return false - } - if (filter != other.filter) { - return false - } - if (optional != other.optional) { - return false - } - - return true - } - - override fun hashCode(): Int { - var result = path.contentHashCode() - result = 31 * result + (id?.hashCode() ?: 0) - result = 31 * result + (purpose?.hashCode() ?: 0) - result = 31 * result + (name?.hashCode() ?: 0) - result = 31 * result + (filter?.hashCode() ?: 0) - result = 31 * result + optional.hashCode() - return result - } - } - - @Serializable(with = LimitDisclosure.LimitDisclosureSerializer::class) - enum class LimitDisclosure(val value: String) { - REQUIRED("required"), - PREFERRED("preferred"); - - // Custom serializer for the enum - object LimitDisclosureSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("LimitDisclosure", PrimitiveKind.STRING) - - override fun serialize(encoder: Encoder, value: LimitDisclosure) { - encoder.encodeString(value.value) - } - - override fun deserialize(decoder: Decoder): LimitDisclosure { - val stringValue = decoder.decodeString() - return entries.firstOrNull { it.value == stringValue } - ?: throw SerializationException("Unknown value: $stringValue") - } - } - } + data class Filter( + val type: String, + val pattern: String + ) override fun equals(other: Any?): Boolean { if (this === other) { @@ -151,19 +93,24 @@ data class JWTPresentationDefinitionRequest( return false } - other as Constraints + other as Field - if (fields != null) { - if (other.fields == null) { - return false - } - if (!fields.contentEquals(other.fields)) { - return false - } - } else if (other.fields != null) { + if (!path.contentEquals(other.path)) { + return false + } + if (id != other.id) { + return false + } + if (purpose != other.purpose) { + return false + } + if (name != other.name) { return false } - if (limitDisclosure != other.limitDisclosure) { + if (filter != other.filter) { + return false + } + if (optional != other.optional) { return false } @@ -171,48 +118,109 @@ data class JWTPresentationDefinitionRequest( } override fun hashCode(): Int { - var result = fields?.contentHashCode() ?: 0 - result = 31 * result + (limitDisclosure?.hashCode() ?: 0) + var result = path.contentHashCode() + result = 31 * result + (id?.hashCode() ?: 0) + result = 31 * result + (purpose?.hashCode() ?: 0) + result = 31 * result + (name?.hashCode() ?: 0) + result = 31 * result + (filter?.hashCode() ?: 0) + result = 31 * result + optional.hashCode() return result } } - @Serializable - data class PresentationFormat( - @SerialName("jwt") - val jwt: JwtFormat? = null, - ) + @Serializable(with = LimitDisclosure.LimitDisclosureSerializer::class) + enum class LimitDisclosure(val value: String) { + REQUIRED("required"), + PREFERRED("preferred"); - @Serializable - data class JwtFormat( - val alg: List - ) - } + // Custom serializer for the enum + object LimitDisclosureSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("LimitDisclosure", PrimitiveKind.STRING) - override fun equals(other: Any?): Boolean { - if (this === other) { - return true - } - if (other == null || this::class != other::class) { - return false + override fun serialize(encoder: Encoder, value: LimitDisclosure) { + encoder.encodeString(value.value) + } + + override fun deserialize(decoder: Decoder): LimitDisclosure { + val stringValue = decoder.decodeString() + return entries.firstOrNull { it.value == stringValue } + ?: throw SerializationException("Unknown value: $stringValue") + } + } } - other as PresentationDefinition + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (javaClass != other?.javaClass) { + return false + } + + other as Constraints + + if (fields != null) { + if (other.fields == null) { + return false + } + if (!fields.contentEquals(other.fields)) { + return false + } + } else if (other.fields != null) { + return false + } + if (limitDisclosure != other.limitDisclosure) { + return false + } - if (id != other.id) { - return false + return true } - if (!inputDescriptors.contentEquals(other.inputDescriptors)) { - return false + + override fun hashCode(): Int { + var result = fields?.contentHashCode() ?: 0 + result = 31 * result + (limitDisclosure?.hashCode() ?: 0) + return result } - return format == other.format } - override fun hashCode(): Int { - var result = id?.hashCode() ?: 0 - result = 31 * result + inputDescriptors.contentHashCode() - result = 31 * result + format.hashCode() - return result + @Serializable + data class PresentationFormat( + @SerialName("jwt") + val jwt: JwtFormat? = null, + @SerialName("sdJwt") + val sdjwt: JwtFormat? = null, + ) + + @Serializable + data class JwtFormat( + val alg: List + ) + } + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + if (other == null || this::class != other::class) { + return false + } + + other as PresentationDefinition + + if (id != other.id) { + return false } + if (!inputDescriptors.contentEquals(other.inputDescriptors)) { + return false + } + return format == other.format + } + + override fun hashCode(): Int { + var result = id?.hashCode() ?: 0 + result = 31 * result + inputDescriptors.contentHashCode() + result = 31 * result + format.hashCode() + return result } } diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/PresentationSubmission.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/PresentationSubmission.kt index 106f473e1..9958b38d0 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/PresentationSubmission.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/PresentationSubmission.kt @@ -6,7 +6,8 @@ import java.util.UUID enum class DescriptorItemFormat(val value: String) { JWT_VC("jwt_vc"), - JWT_VP("jwt_vp") + JWT_VP("jwt_vp"), + SD_JWT_VP("sdjwt") } @Serializable diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/SDJWTCredential.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/SDJWTCredential.kt index c879ed33b..c779a2e81 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/SDJWTCredential.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/models/SDJWTCredential.kt @@ -1,4 +1,5 @@ package org.hyperledger.identus.walletsdk.pollux.models + import eu.europa.ec.eudi.sdjwt.JsonPointer import eu.europa.ec.eudi.sdjwt.JwtAndClaims import eu.europa.ec.eudi.sdjwt.JwtSignatureVerifier @@ -12,6 +13,7 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Transient import kotlinx.serialization.builtins.ArraySerializer import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonElement @@ -22,10 +24,10 @@ import org.hyperledger.identus.walletsdk.domain.models.Claim import org.hyperledger.identus.walletsdk.domain.models.ClaimType import org.hyperledger.identus.walletsdk.domain.models.Credential import org.hyperledger.identus.walletsdk.domain.models.CredentialOperationsOptions +import org.hyperledger.identus.walletsdk.domain.models.PolluxError import org.hyperledger.identus.walletsdk.domain.models.ProvableCredential import org.hyperledger.identus.walletsdk.domain.models.StorableCredential -import org.hyperledger.identus.walletsdk.domain.models.keyManagement.ExportableKey -import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey +import java.util.* @OptIn(ExperimentalSerializationApi::class) data class SDJWTCredential( @@ -36,10 +38,11 @@ data class SDJWTCredential( get() = sdjwtString @Transient - override val issuer: String = sdjwt.jwt.second.get("iss").toString() + override val issuer: String = sdjwt.jwt.second["iss"]?.jsonPrimitive?.content + ?: throw PolluxError.InvalidJWTCredential("SD-JWT must contain issuer") override val subject: String? - get() = sdjwt.jwt.second.get("sub").toString() + get() = sdjwt.jwt.second["sub"]?.jsonPrimitive?.content override val claims: Array get() { @@ -52,38 +55,66 @@ data class SDJWTCredential( override val properties: Map get() { val properties = mutableMapOf() - properties["nbf"] = sdjwt.jwt.second.get("nbf").toString() - properties["jti"] = sdjwt.jwt.second.get("sub").toString() - properties["aud"] = sdjwt.jwt.second.get("aud").toString() + properties["nbf"] = sdjwt.jwt.second["nbf"]?.jsonPrimitive?.content + properties["jti"] = sdjwt.jwt.second["sub"]?.jsonPrimitive?.content + properties["aud"] = sdjwt.jwt.second["aud"]?.jsonPrimitive?.content properties["id"] = id - sdjwt.jwt.second.get("exp").toString().let { properties["exp"] = it } + sdjwt.jwt.second["exp"]?.jsonPrimitive?.content.let { properties["exp"] = it } return properties.toMap() } override var revoked: Boolean? = null - override suspend fun presentation(request: ByteArray, options: List): String { - var exportableKeyOption: PrivateKey? = null + override suspend fun presentation( + attachmentFormat: String, + request: ByteArray, + options: List + ): String { + val jwtPresentationDefinitionRequest = + Json.decodeFromString(String(request, Charsets.UTF_8)) + val descriptorItems = + jwtPresentationDefinitionRequest.presentationDefinition.inputDescriptors.map { inputDescriptor -> + if (inputDescriptor.format != null && (inputDescriptor.format.sdjwt == null || inputDescriptor.format.sdjwt.alg.isEmpty())) { + throw PolluxError.InvalidCredentialDefinitionError() + } + PresentationSubmission.Submission.DescriptorItem( + id = inputDescriptor.id, + format = DescriptorItemFormat.SD_JWT_VP.value, + path = "$.verifiablePresentation[0]" + ) + }.toTypedArray() + var disclosingClaims: List? = null for (option in options) { when (option) { - is CredentialOperationsOptions.ExportableKey -> exportableKeyOption = option.key is CredentialOperationsOptions.DisclosingClaims -> disclosingClaims = option.claims else -> {} } } - val inluded = disclosingClaims + val included = disclosingClaims ?.mapNotNull { JsonPointer.parse(it) } ?.toSet() - val presentation = sdjwt.present(inluded!!) - return presentation!!.serialize { (jwt, _) -> jwt } + + val presentation = sdjwt.present(included!!) + val sdjwtString = presentation!!.serialize { (jwt, _) -> jwt } + + return Json.encodeToString( + PresentationSubmission( + presentationSubmission = PresentationSubmission.Submission( + definitionId = jwtPresentationDefinitionRequest.presentationDefinition.id + ?: UUID.randomUUID().toString(), + descriptorMap = descriptorItems + ), + verifiablePresentation = arrayOf(sdjwtString) + ) + ) } /** - * Converts the current instance of [JWTCredential] to a [StorableCredential]. + * Converts the current instance of [SDJWTCredential] to a [StorableCredential]. * * @return The converted [StorableCredential]. */ @@ -93,7 +124,7 @@ data class SDJWTCredential( override val id: String get() = c.id override val recoveryId: String - get() = "jwt+credential" + get() = "sd-jwt+credential" override val credentialData: ByteArray get() = c.id.toByteArray() @@ -109,7 +140,7 @@ data class SDJWTCredential( override val credentialSchema: String? get() = null override val validUntil: String? - get() = c.sdjwt.jwt.second.get("exp").toString().toString() + get() = c.sdjwt.jwt.second["exp"]?.jsonPrimitive?.content override var revoked: Boolean? = c.revoked override val availableClaims: Array get() = c.claims.map { it.key }.toTypedArray() @@ -122,12 +153,12 @@ data class SDJWTCredential( override val properties: Map get() { val properties = mutableMapOf() - properties["nbf"] = sdjwt.jwt.second.get("nbf").toString() - properties["jti"] = sdjwt.jwt.second.get("jti").toString() - properties["aud"] = sdjwt.jwt.second.get("aud").toString() + properties["nbf"] = sdjwt.jwt.second["nbf"]?.jsonPrimitive?.content + properties["jti"] = sdjwt.jwt.second["jti"]?.jsonPrimitive?.content + properties["aud"] = sdjwt.jwt.second["aud"]?.jsonPrimitive?.content properties["id"] = id - sdjwt.jwt.second.get("exp").toString().let { properties["exp"] = it } + sdjwt.jwt.second["exp"]?.jsonPrimitive?.content.let { properties["exp"] = it } return properties.toMap() } @@ -159,7 +190,8 @@ data class SDJWTCredential( fun fromSDJwtString(sdjwtString: String): SDJWTCredential { var credential: SDJWTCredential runBlocking { - val sdjwt = SdJwtVerifier.verifyIssuance(JwtSignatureVerifier.NoSignatureValidation, sdjwtString).getOrThrow() + val sdjwt = + SdJwtVerifier.verifyIssuance(JwtSignatureVerifier.NoSignatureValidation, sdjwtString).getOrThrow() credential = SDJWTCredential(sdjwtString, sdjwt) } return credential diff --git a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/utils/BitString.kt b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/utils/BitString.kt index d2769dfb1..7ee9ecfc8 100644 --- a/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/utils/BitString.kt +++ b/edge-agent-sdk/src/commonMain/kotlin/org/hyperledger/identus/walletsdk/pollux/utils/BitString.kt @@ -49,5 +49,4 @@ class BitString(byteArray: ByteArray) { } return result } -// } } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/castor/ApolloMock.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/castor/ApolloMock.kt index bec4c62b9..489bb1af8 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/castor/ApolloMock.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/castor/ApolloMock.kt @@ -14,7 +14,6 @@ import org.hyperledger.identus.walletsdk.domain.models.keyManagement.KeyPair import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PublicKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.StorableKey -import org.hyperledger.identus.walletsdk.domain.models.keyManagement.StorablePrivateKey class ApolloMock : Apollo { var createRandomMnemonicsReturn: Array = emptyArray() @@ -62,10 +61,6 @@ class ApolloMock : Apollo { TODO("Not yet implemented") } - override fun restorePrivateKey(storablePrivateKey: StorablePrivateKey): PrivateKey { - TODO("Not yet implemented") - } - override fun restorePrivateKey(restorationIdentifier: String, privateKeyData: String): PrivateKey { TODO("Not yet implemented") } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/castor/PrismDIDPublicKeyTests.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/castor/PrismDIDPublicKeyTests.kt index 22575cac4..0242750e8 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/castor/PrismDIDPublicKeyTests.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/castor/PrismDIDPublicKeyTests.kt @@ -3,12 +3,12 @@ package org.hyperledger.identus.walletsdk.castor import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.hyperledger.identus.protos.PublicKey +import org.hyperledger.identus.walletsdk.apollo.ApolloImpl import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519KeyPair import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519PrivateKey import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519PublicKey import org.hyperledger.identus.walletsdk.castor.did.prismdid.PrismDIDPublicKey import org.hyperledger.identus.walletsdk.castor.did.prismdid.id -import org.junit.Ignore import org.junit.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals @@ -16,18 +16,18 @@ import kotlin.test.assertEquals class PrismDIDPublicKeyTests { @OptIn(ExperimentalCoroutinesApi::class) - @Ignore("PrismDIDPublicKey requires Secp256k1Lib to be an interface in order to mock its result. Once that is done this test can be added back.") +// @Ignore("PrismDIDPublicKey requires Secp256k1Lib to be an interface in order to mock its result. Once that is done this test can be added back.") @Test fun it_should_parse_proto_toPrismDIDPublicKey() = runTest { - val apollo = ApolloMock() - val seed = apollo.createRandomSeed(passphrase = "mnemonics").seed + val apollo = ApolloImpl() +// val seed = apollo.createRandomSeed(passphrase = "mnemonics").seed val keyPair = Ed25519KeyPair( privateKey = Ed25519PrivateKey(ByteArray(0)), publicKey = Ed25519PublicKey(ByteArray(0)) ) val publicKey = PrismDIDPublicKey( - apollo = ApolloMock(), + apollo = apollo, id = PrismDIDPublicKey.Usage.MASTER_KEY.id(0), usage = PrismDIDPublicKey.Usage.MASTER_KEY, keyData = keyPair.publicKey diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/ApolloMock.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/ApolloMock.kt index 7ef245ba9..305e2557a 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/ApolloMock.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/ApolloMock.kt @@ -1,10 +1,13 @@ package org.hyperledger.identus.walletsdk.edgeagent +import io.iohk.atala.prism.didcomm.didpeer.base64.base64UrlDecodedBytes +import org.bouncycastle.util.encoders.Hex import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519KeyPair import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519PrivateKey import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519PublicKey import org.hyperledger.identus.walletsdk.apollo.utils.Secp256k1PrivateKey import org.hyperledger.identus.walletsdk.domain.buildingblocks.Apollo +import org.hyperledger.identus.walletsdk.domain.models.Curve import org.hyperledger.identus.walletsdk.domain.models.Seed import org.hyperledger.identus.walletsdk.domain.models.SeedWords import org.hyperledger.identus.walletsdk.domain.models.keyManagement.JWK @@ -13,9 +16,10 @@ import org.hyperledger.identus.walletsdk.domain.models.keyManagement.KeyPair import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PublicKey import org.hyperledger.identus.walletsdk.domain.models.keyManagement.StorableKey -import org.hyperledger.identus.walletsdk.domain.models.keyManagement.StorablePrivateKey class ApolloMock : Apollo { + val validRawBase64UrlSecp256k1Sk = Hex.decode("67E56582298859DDAE725F972992A07C6C4FB9F62A8FFF58CE3CA926A1063530".lowercase()) + val validRawBase64UrlEd25519Sk = "JLIJQ5jlkyqtGmtOth6yggJLLC0zuRhUPiBhd1-rGPs".base64UrlDecodedBytes var createRandomMnemonicsReturn: Array = emptyArray() var createSeedReturn: Seed = Seed(ByteArray(0)) var createRandomSeedReturn: SeedWords = SeedWords(emptyArray(), Seed(ByteArray(0))) @@ -36,7 +40,13 @@ class ApolloMock : Apollo { } override fun createPrivateKey(properties: Map): PrivateKey { - return createPrivateKey ?: Secp256k1PrivateKey(ByteArray(0)) + val curve = properties["curve"] + if (curve === Curve.SECP256K1.value) { + return Secp256k1PrivateKey(validRawBase64UrlSecp256k1Sk) + } else if (curve === Curve.ED25519.value) { + return Ed25519PrivateKey(validRawBase64UrlEd25519Sk) + } + TODO("Not yet implemented") } override fun createPublicKey(properties: Map): PublicKey { @@ -55,10 +65,6 @@ class ApolloMock : Apollo { TODO("Not yet implemented") } - override fun restorePrivateKey(storablePrivateKey: StorablePrivateKey): PrivateKey { - TODO("Not yet implemented") - } - override fun restorePrivateKey(restorationIdentifier: String, privateKeyData: String): PrivateKey { TODO("Not yet implemented") } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/CastorMock.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/CastorMock.kt index d43c2e879..dfae652e1 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/CastorMock.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/CastorMock.kt @@ -25,7 +25,8 @@ class CastorMock : Castor { override fun createPrismDID( masterPublicKey: PublicKey, - services: Array? + services: Array?, + authenticationKeys: Array ): DID { return createPrismDIDReturn ?: throw Exception("createPrismDID() not implemented in mock") } 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 f1ce8e82e..fd4629b31 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 @@ -51,6 +51,7 @@ import org.hyperledger.identus.walletsdk.domain.models.DIDPair import org.hyperledger.identus.walletsdk.domain.models.DIDResolver import org.hyperledger.identus.walletsdk.domain.models.DIDUrl import org.hyperledger.identus.walletsdk.domain.models.HttpResponse +import org.hyperledger.identus.walletsdk.domain.models.InputFieldFilter import org.hyperledger.identus.walletsdk.domain.models.JWTPresentationClaims import org.hyperledger.identus.walletsdk.domain.models.KeyCurve import org.hyperledger.identus.walletsdk.domain.models.Mediator @@ -58,6 +59,7 @@ import org.hyperledger.identus.walletsdk.domain.models.Message import org.hyperledger.identus.walletsdk.domain.models.PolluxError import org.hyperledger.identus.walletsdk.domain.models.ProvableCredential import org.hyperledger.identus.walletsdk.domain.models.RequestedAttributes +import org.hyperledger.identus.walletsdk.domain.models.SDJWTPresentationClaims import org.hyperledger.identus.walletsdk.domain.models.Seed import org.hyperledger.identus.walletsdk.domain.models.Signature import org.hyperledger.identus.walletsdk.domain.models.keyManagement.CurveKey @@ -76,6 +78,7 @@ import org.hyperledger.identus.walletsdk.edgeagent.protocols.outOfBand.Connectio 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 +import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.SDJWTPreparePresentationOptions import org.hyperledger.identus.walletsdk.logger.LoggerMock import org.hyperledger.identus.walletsdk.mercury.ApiMock import org.hyperledger.identus.walletsdk.pluto.CredentialRecovery @@ -88,6 +91,8 @@ import org.hyperledger.identus.walletsdk.pollux.models.AnonCredential import org.hyperledger.identus.walletsdk.pollux.models.CredentialRequestMeta import org.hyperledger.identus.walletsdk.pollux.models.JWTCredential import org.hyperledger.identus.walletsdk.pollux.models.PresentationSubmission +import org.hyperledger.identus.walletsdk.pollux.models.SDJWTCredential +import org.hyperledger.identus.walletsdk.pollux.models.SDJWTPresentationDefinitionRequest import org.junit.Assert.assertNotEquals import org.junit.Assert.assertTrue import org.junit.Before @@ -103,6 +108,7 @@ import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.spy +import kotlin.test.assertContentEquals import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse @@ -369,6 +375,26 @@ class EdgeAgentTests { assertTrue { plutoMockOld.wasStorePrismDIDAndPrivateKeysCalled } } + @Test + fun testCreateNewPrismDID_whenVcSdJwt_then() = runTest { + val apollo = ApolloImpl() + val castor = CastorImpl(apollo) + val agent = EdgeAgent( + apollo = apolloMockOld, + castor = castor, + pluto = plutoMockOld, + mercury = mercuryMockOld, + pollux = polluxMockOld, + connectionManager = connectionManagerOld, + seed = seed, + api = null, + logger = LoggerMock(), + agentOptions = AgentOptions() + ) + plutoMockOld.getPrismLastKeyPathIndexReturn = flow { emit(0) } + val newDID = agent.createNewPrismDID() + } + @Test fun testCreateNewPeerDID_shouldCreateNewDID_whenCalled() = runTest { val agent = spy( @@ -1348,19 +1374,11 @@ class EdgeAgentTests { ).thenReturn(privateKey) val presentationSubmissionString = - "{\"presentation_submission\":{\"id\":\"00000000-c224-45d7-0000-0000732f4932\",\"definition_id\":\"32f54163-7166-48f1-93d8-ff217bdb0653\",\"descriptor_map\":[{\"id\":\"wa_driver_license\",\"format\":\"jwt\",\"path\":\"$.verifiablePresentation[0]\"}]},\"verifiablePresentation\":[\"eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206MjU3MTlhOTZiMTUxMjA3MTY5ODFhODQzMGFkMGNiOTY4ZGQ1MzQwNzM1OTNjOGNkM2YxZDI3YTY4MDRlYzUwZTpDcG9DQ3BjQ0Vsb0tCV3RsZVMweEVBSkNUd29KYzJWamNESTFObXN4RWlBRW9TQ241dHlEYTZZNnItSW1TcXBKOFkxbWo3SkMzX29VekUwTnl5RWlDQm9nc2dOYWVSZGNDUkdQbGU4MlZ2OXRKZk53bDZyZzZWY2hSM09xaGlWYlRhOFNXd29HWVhWMGFDMHhFQVJDVHdvSmMyVmpjREkxTm1zeEVpRE1rQmQ2RnRpb0prM1hPRnUtX2N5NVhtUi00dFVRMk5MR2lXOGFJU29ta1JvZzZTZGU5UHduRzBRMFNCVG1GU1REYlNLQnZJVjZDVExYcmpJSnR0ZUdJbUFTWEFvSGJXRnpkR1Z5TUJBQlFrOEtDWE5sWTNBeU5UWnJNUklnTzcxMG10MVdfaXhEeVFNM3hJczdUcGpMQ05PRFF4Z1ZoeDVzaGZLTlgxb2FJSFdQcnc3SVVLbGZpYlF0eDZKazRUU2pnY1dOT2ZjT3RVOUQ5UHVaN1Q5dCIsInN1YiI6ImRpZDpwcmlzbTpiZWVhNTIzNGFmNDY4MDQ3MTRkOGVhOGVjNzdiNjZjYzdmM2U4MTVjNjhhYmI0NzVmMjU0Y2Y5YzMwNjI2NzYzOkNzY0JDc1FCRW1RS0QyRjFkR2hsYm5ScFkyRjBhVzl1TUJBRVFrOEtDWE5sWTNBeU5UWnJNUklnZVNnLTJPTzFKZG5welVPQml0eklpY1hkZnplQWNUZldBTi1ZQ2V1Q2J5SWFJSlE0R1RJMzB0YVZpd2NoVDNlMG5MWEJTNDNCNGo5amxzbEtvMlpsZFh6akVsd0tCMjFoYzNSbGNqQVFBVUpQQ2dselpXTndNalUyYXpFU0lIa29QdGpqdFNYWjZjMURnWXJjeUluRjNYODNnSEUzMWdEZm1BbnJnbThpR2lDVU9Ca3lOOUxXbFlzSElVOTN0Snkxd1V1TndlSV9ZNWJKU3FObVpYVjg0dyIsIm5iZiI6MTY4NTYzMTk5NSwiZXhwIjoxNjg1NjM1NTk1LCJ2YyI6eyJjcmVkZW50aWFsU3ViamVjdCI6eyJhZGRpdGlvbmFsUHJvcDIiOiJUZXN0MyIsImlkIjoiZGlkOnByaXNtOmJlZWE1MjM0YWY0NjgwNDcxNGQ4ZWE4ZWM3N2I2NmNjN2YzZTgxNWM2OGFiYjQ3NWYyNTRjZjljMzA2MjY3NjM6Q3NjQkNzUUJFbVFLRDJGMWRHaGxiblJwWTJGMGFXOXVNQkFFUWs4S0NYTmxZM0F5TlRack1SSWdlU2ctMk9PMUpkbnB6VU9CaXR6SWljWGRmemVBY1RmV0FOLVlDZXVDYnlJYUlKUTRHVEkzMHRhVml3Y2hUM2UwbkxYQlM0M0I0ajlqbHNsS28yWmxkWHpqRWx3S0IyMWhjM1JsY2pBUUFVSlBDZ2x6WldOd01qVTJhekVTSUhrb1B0amp0U1haNmMxRGdZcmN5SW5GM1g4M2dIRTMxZ0RmbUFucmdtOGlHaUNVT0JreU45TFdsWXNISVU5M3RKeTF3VXVOd2VJX1k1YkpTcU5tWlhWODR3In0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiQGNvbnRleHQiOlsiaHR0cHM6XC9cL3d3dy53My5vcmdcLzIwMThcL2NyZWRlbnRpYWxzXC92MSJdfX0.x0SF17Y0VCDmt7HceOdTxfHlofsZmY18Rn6VQb0-r-k_Bm3hTi1-k2vkdjB25hdxyTCvxam-AkAP-Ag3Ahn5Ng\"]}" - val presentationSubmission = presentationSubmissionString + """{"presentation_submission":{"id":"2ada28b3-7866-4769-8c0a-31de12c51ee7","definition_id":"32f54163-7166-48f1-93d8-ff217bdb0653","descriptor_map":[{"id":"wa_driver_license","format":"jwt_vp","path":"${'$'}.verifiablePresentation[0]","path_nested":{"id":"wa_driver_license","format":"jwt_vc","path":"${'$'}.vp.verifiableCredential[0]"}}]},"verifiablePresentation":["eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206YmVlYTUyMzRhZjQ2ODA0NzE0ZDhlYThlYzc3YjY2Y2M3ZjNlODE1YzY4YWJiNDc1ZjI1NGNmOWMzMDYyNjc2MzpDc2NCQ3NRQkVtUUtEMkYxZEdobGJuUnBZMkYwYVc5dU1CQUVRazhLQ1hObFkzQXlOVFpyTVJJZ2VTZy0yT08xSmRucHpVT0JpdHpJaWNYZGZ6ZUFjVGZXQU4tWUNldUNieUlhSUpRNEdUSTMwdGFWaXdjaFQzZTBuTFhCUzQzQjRqOWpsc2xLbzJabGRYempFbHdLQjIxaGMzUmxjakFRQVVKUENnbHpaV053TWpVMmF6RVNJSGtvUHRqanRTWFo2YzFEZ1lyY3lJbkYzWDgzZ0hFMzFnRGZtQW5yZ204aUdpQ1VPQmt5TjlMV2xZc0hJVTkzdEp5MXdVdU53ZUlfWTViSlNxTm1aWFY4NHciLCJhdWQiOiJkb21haW4iLCJ2cCI6eyJAY29udGV4dCI6WyJodHRwczpcL1wvd3d3LnczLm9yZ1wvMjAxOFwvY3JlZGVudGlhbHNcL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZVByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSkZVekkxTmtzaWZRLmV5SnBjM01pT2lKa2FXUTZjSEpwYzIwNk1qVTNNVGxoT1RaaU1UVXhNakEzTVRZNU9ERmhPRFF6TUdGa01HTmlPVFk0WkdRMU16UXdOek0xT1ROak9HTmtNMll4WkRJM1lUWTRNRFJsWXpVd1pUcERjRzlEUTNCalEwVnNiMHRDVjNSc1pWTXdlRVZCU2tOVWQyOUtZekpXYW1ORVNURk9iWE40UldsQlJXOVRRMjQxZEhsRVlUWlpObkl0U1cxVGNYQktPRmt4YldvM1NrTXpYMjlWZWtVd1RubDVSV2xEUW05bmMyZE9ZV1ZTWkdORFVrZFFiR1U0TWxaMk9YUktaazUzYkRaeVp6WldZMmhTTTA5eGFHbFdZbFJoT0ZOWGQyOUhXVmhXTUdGRE1IaEZRVkpEVkhkdlNtTXlWbXBqUkVreFRtMXplRVZwUkUxclFtUTJSblJwYjBwck0xaFBSblV0WDJONU5WaHRVaTAwZEZWUk1rNU1SMmxYT0dGSlUyOXRhMUp2WnpaVFpHVTVVSGR1UnpCUk1GTkNWRzFHVTFSRVlsTkxRblpKVmpaRFZFeFljbXBKU25SMFpVZEpiVUZUV0VGdlNHSlhSbnBrUjFaNVRVSkJRbEZyT0V0RFdFNXNXVE5CZVU1VVduSk5Va2xuVHpjeE1HMTBNVmRmYVhoRWVWRk5NM2hKY3pkVWNHcE1RMDVQUkZGNFoxWm9lRFZ6YUdaTFRsZ3hiMkZKU0ZkUWNuYzNTVlZMYkdacFlsRjBlRFpLYXpSVVUycG5ZMWRPVDJaalQzUlZPVVE1VUhWYU4xUTVkQ0lzSW5OMVlpSTZJbVJwWkRwd2NtbHpiVHBpWldWaE5USXpOR0ZtTkRZNE1EUTNNVFJrT0dWaE9HVmpOemRpTmpaall6ZG1NMlU0TVRWak5qaGhZbUkwTnpWbU1qVTBZMlk1WXpNd05qSTJOell6T2tOelkwSkRjMUZDUlcxUlMwUXlSakZrUjJoc1ltNVNjRmt5UmpCaFZ6bDFUVUpCUlZGck9FdERXRTVzV1ROQmVVNVVXbkpOVWtsblpWTm5MVEpQVHpGS1pHNXdlbFZQUW1sMGVrbHBZMWhrWm5wbFFXTlVabGRCVGkxWlEyVjFRMko1U1dGSlNsRTBSMVJKTXpCMFlWWnBkMk5vVkRObE1HNU1XRUpUTkROQ05HbzVhbXh6YkV0dk1scHNaRmg2YWtWc2QwdENNakZvWXpOU2JHTnFRVkZCVlVwUVEyZHNlbHBYVG5kTmFsVXlZWHBGVTBsSWEyOVFkR3BxZEZOWVdqWmpNVVJuV1hKamVVbHVSak5ZT0ROblNFVXpNV2RFWm0xQmJuSm5iVGhwUjJsRFZVOUNhM2xPT1V4WGJGbHpTRWxWT1ROMFNua3hkMVYxVG5kbFNWOVpOV0pLVTNGT2JWcFlWamcwZHlJc0ltNWlaaUk2TVRZNE5UWXpNVGs1TlN3aVpYaHdJam94TmpnMU5qTTFOVGsxTENKMll5STZleUpqY21Wa1pXNTBhV0ZzVTNWaWFtVmpkQ0k2ZXlKaFpHUnBkR2x2Ym1Gc1VISnZjRElpT2lKVVpYTjBNeUlzSW1sa0lqb2laR2xrT25CeWFYTnRPbUpsWldFMU1qTTBZV1kwTmpnd05EY3hOR1E0WldFNFpXTTNOMkkyTm1Oak4yWXpaVGd4TldNMk9HRmlZalEzTldZeU5UUmpaamxqTXpBMk1qWTNOak02UTNOalFrTnpVVUpGYlZGTFJESkdNV1JIYUd4aWJsSndXVEpHTUdGWE9YVk5Ra0ZGVVdzNFMwTllUbXhaTTBGNVRsUmFjazFTU1dkbFUyY3RNazlQTVVwa2JuQjZWVTlDYVhSNlNXbGpXR1JtZW1WQlkxUm1WMEZPTFZsRFpYVkRZbmxKWVVsS1VUUkhWRWt6TUhSaFZtbDNZMmhVTTJVd2JreFlRbE0wTTBJMGFqbHFiSE5zUzI4eVdteGtXSHBxUld4M1MwSXlNV2hqTTFKc1kycEJVVUZWU2xCRFoyeDZXbGRPZDAxcVZUSmhla1ZUU1VocmIxQjBhbXAwVTFoYU5tTXhSR2RaY21ONVNXNUdNMWc0TTJkSVJUTXhaMFJtYlVGdWNtZHRPR2xIYVVOVlQwSnJlVTQ1VEZkc1dYTklTVlU1TTNSS2VURjNWWFZPZDJWSlgxazFZa3BUY1U1dFdsaFdPRFIzSW4wc0luUjVjR1VpT2xzaVZtVnlhV1pwWVdKc1pVTnlaV1JsYm5ScFlXd2lYU3dpUUdOdmJuUmxlSFFpT2xzaWFIUjBjSE02WEM5Y0wzZDNkeTUzTXk1dmNtZGNMekl3TVRoY0wyTnlaV1JsYm5ScFlXeHpYQzkyTVNKZGZYMC54MFNGMTdZMFZDRG10N0hjZU9kVHhmSGxvZnNabVkxOFJuNlZRYjAtci1rX0JtM2hUaTEtazJ2a2RqQjI1aGR4eVRDdnhhbS1Ba0FQLUFnM0FobjVOZyJdfSwibm9uY2UiOiJjaGFsbGVuZ2UifQ.pkxVLfL3xLcA_lP6kAFfN4SefD6X0fsKSbGNiXSNn4x7iPewC28zKVeIkrYCzQCV_wAcCrbyBu5KAwICxRsPjQ"]}""" - val presentationDefinitionRequest = - """{"presentation_definition":{"id":"32f54163-7166-48f1-93d8-ff217bdb0653","input_descriptors":[{"id":"wa_driver_license","name":"Washington State Business License","purpose":"We can only allow licensed Washington State business representatives into the WA Business Conference","constraints":{"fields":[{"path":["${'$'}.credentialSubject.dateOfBirth","${'$'}.credentialSubject.dob","${'$'}.vc.credentialSubject.dateOfBirth","${'$'}.vc.credentialSubject.dob"]}]}}],"format":{"jwt":{"alg":["ES256K"]}}},"options":{"domain":"domain","challenge":"challenge"}}""" val credential = JWTCredential.fromJwtString( "eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206MjU3MTlhOTZiMTUxMjA3MTY5ODFhODQzMGFkMGNiOTY4ZGQ1MzQwNzM1OTNjOGNkM2YxZDI3YTY4MDRlYzUwZTpDcG9DQ3BjQ0Vsb0tCV3RsZVMweEVBSkNUd29KYzJWamNESTFObXN4RWlBRW9TQ241dHlEYTZZNnItSW1TcXBKOFkxbWo3SkMzX29VekUwTnl5RWlDQm9nc2dOYWVSZGNDUkdQbGU4MlZ2OXRKZk53bDZyZzZWY2hSM09xaGlWYlRhOFNXd29HWVhWMGFDMHhFQVJDVHdvSmMyVmpjREkxTm1zeEVpRE1rQmQ2RnRpb0prM1hPRnUtX2N5NVhtUi00dFVRMk5MR2lXOGFJU29ta1JvZzZTZGU5UHduRzBRMFNCVG1GU1REYlNLQnZJVjZDVExYcmpJSnR0ZUdJbUFTWEFvSGJXRnpkR1Z5TUJBQlFrOEtDWE5sWTNBeU5UWnJNUklnTzcxMG10MVdfaXhEeVFNM3hJczdUcGpMQ05PRFF4Z1ZoeDVzaGZLTlgxb2FJSFdQcnc3SVVLbGZpYlF0eDZKazRUU2pnY1dOT2ZjT3RVOUQ5UHVaN1Q5dCIsInN1YiI6ImRpZDpwcmlzbTpiZWVhNTIzNGFmNDY4MDQ3MTRkOGVhOGVjNzdiNjZjYzdmM2U4MTVjNjhhYmI0NzVmMjU0Y2Y5YzMwNjI2NzYzOkNzY0JDc1FCRW1RS0QyRjFkR2hsYm5ScFkyRjBhVzl1TUJBRVFrOEtDWE5sWTNBeU5UWnJNUklnZVNnLTJPTzFKZG5welVPQml0eklpY1hkZnplQWNUZldBTi1ZQ2V1Q2J5SWFJSlE0R1RJMzB0YVZpd2NoVDNlMG5MWEJTNDNCNGo5amxzbEtvMlpsZFh6akVsd0tCMjFoYzNSbGNqQVFBVUpQQ2dselpXTndNalUyYXpFU0lIa29QdGpqdFNYWjZjMURnWXJjeUluRjNYODNnSEUzMWdEZm1BbnJnbThpR2lDVU9Ca3lOOUxXbFlzSElVOTN0Snkxd1V1TndlSV9ZNWJKU3FObVpYVjg0dyIsIm5iZiI6MTY4NTYzMTk5NSwiZXhwIjoxNjg1NjM1NTk1LCJ2YyI6eyJjcmVkZW50aWFsU3ViamVjdCI6eyJhZGRpdGlvbmFsUHJvcDIiOiJUZXN0MyIsImlkIjoiZGlkOnByaXNtOmJlZWE1MjM0YWY0NjgwNDcxNGQ4ZWE4ZWM3N2I2NmNjN2YzZTgxNWM2OGFiYjQ3NWYyNTRjZjljMzA2MjY3NjM6Q3NjQkNzUUJFbVFLRDJGMWRHaGxiblJwWTJGMGFXOXVNQkFFUWs4S0NYTmxZM0F5TlRack1SSWdlU2ctMk9PMUpkbnB6VU9CaXR6SWljWGRmemVBY1RmV0FOLVlDZXVDYnlJYUlKUTRHVEkzMHRhVml3Y2hUM2UwbkxYQlM0M0I0ajlqbHNsS28yWmxkWHpqRWx3S0IyMWhjM1JsY2pBUUFVSlBDZ2x6WldOd01qVTJhekVTSUhrb1B0amp0U1haNmMxRGdZcmN5SW5GM1g4M2dIRTMxZ0RmbUFucmdtOGlHaUNVT0JreU45TFdsWXNISVU5M3RKeTF3VXVOd2VJX1k1YkpTcU5tWlhWODR3In0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiQGNvbnRleHQiOlsiaHR0cHM6XC9cL3d3dy53My5vcmdcLzIwMThcL2NyZWRlbnRpYWxzXC92MSJdfX0.x0SF17Y0VCDmt7HceOdTxfHlofsZmY18Rn6VQb0-r-k_Bm3hTi1-k2vkdjB25hdxyTCvxam-AkAP-Ag3Ahn5Ng" ) - // Mock createPresentationSubmission response - `when`(polluxMock.createJWTPresentationSubmission(any(), any(), any())).thenReturn( -// `when`(polluxMock.createJWTPresentationSubmission(presentationDefinitionRequest, credential, privateKeys.first())).thenReturn( - presentationSubmission - ) val agent = EdgeAgent( apollo = apolloMock, @@ -1394,8 +1412,16 @@ class EdgeAgentTests { val actualPresentationSubmission = Json.decodeFromString(attachmentDataString) assertEquals( - expectedPresentationSubmission, - actualPresentationSubmission + expectedPresentationSubmission.presentationSubmission.definitionId, + actualPresentationSubmission.presentationSubmission.definitionId + ) + assertContentEquals( + expectedPresentationSubmission.presentationSubmission.descriptorMap, + actualPresentationSubmission.presentationSubmission.descriptorMap + ) + assertEquals( + expectedPresentationSubmission.verifiablePresentation.size, + actualPresentationSubmission.verifiablePresentation.size ) } @@ -1750,6 +1776,11 @@ class EdgeAgentTests { JWTCredential.fromJwtString(jwtString).toStorableCredential() } + RestorationID.SDJWT -> { + val jwtString = it.credentialData.base64UrlDecoded + SDJWTCredential.fromSDJwtString(jwtString).toStorableCredential() + } + RestorationID.ANONCRED -> { val data = it.credentialData.base64UrlDecodedBytes PlutoRestoreTask.AnonCredentialBackUp.fromStorableData(data) @@ -1920,6 +1951,215 @@ class EdgeAgentTests { assertTrue(json.jsonObject.containsKey("presentation_definition")) } + @Test + fun `test initiatePresentationRequest SD-JWT`() = runTest { + val agent = spy( + EdgeAgent( + apollo = apolloMock, + castor = castorMock, + pluto = plutoMock, + mercury = mercuryMock, + pollux = PolluxImpl(apolloMock, castorMock), + connectionManager = connectionManagerMock, + seed = seed, + api = null, + logger = LoggerMock() + ) + ) + + val mediatorHandlerMock = mock() + `when`(connectionManagerMock.mediationHandler).thenReturn(mediatorHandlerMock) + val mediator = Mediator( + id = UUID.randomUUID().toString(), + mediatorDID = DID("did:peer:mediatordid"), + hostDID = DID("did:peer:hostdid"), + routingDID = DID("did:peer:routingdid") + ) + `when`(mediatorHandlerMock.mediator).thenReturn(mediator) + + val toDid = DID("did:peer:fdsafdsa") + val fromDid = DID("did:peer:asdf") + doReturn(fromDid).`when`(agent).createNewPeerDID(updateMediator = true) + + val vmAuthentication = DIDDocument.VerificationMethod( + id = DIDUrl(DID("2", "1", "0")), + controller = DID("2", "2", "0"), + type = Curve.ED25519.value, + publicKeyJwk = mapOf("crv" to Curve.ED25519.value, "x" to "") + ) + + val vmKeyAgreement = DIDDocument.VerificationMethod( + id = DIDUrl(DID("3", "1", "0")), + controller = DID("3", "2", "0"), + type = Curve.X25519.value, + publicKeyJwk = mapOf("crv" to Curve.X25519.value, "x" to "") + ) + + val resolverMock = mock() + val didDoc = DIDDocument( + id = DID("did:prism:asdfasdf"), + coreProperties = arrayOf( + DIDDocument.Authentication( + urls = emptyArray(), + verificationMethods = arrayOf(vmAuthentication, vmKeyAgreement) + ) + ) + ) + // Mock resolve did response + `when`(castorMock.resolveDID(any())).thenReturn(didDoc) + `when`(resolverMock.resolve(any())).thenReturn(didDoc) + + agent.initiatePresentationRequest( + type = CredentialType.SDJWT, + toDID = toDid, + presentationClaims = SDJWTPresentationClaims( + claims = mapOf( + "first_name" to InputFieldFilter( + type = "string", + pattern = "Wonderland" + ), + "last_name" to InputFieldFilter( + type = "string", + pattern = "Alice" + ), + "emailAddress" to InputFieldFilter( + type = "string", + pattern = "alice@wonderland.com" + ) + ) + ), + domain = "domain", + challenge = "challenge" + ) + + val captor = argumentCaptor() + verify(connectionManagerMock).sendMessage(captor.capture()) + val sentMessage = captor.firstValue + assertEquals(fromDid.toString(), sentMessage.from.toString()) + assertEquals(toDid.toString(), sentMessage.to.toString()) + assertEquals(1, sentMessage.attachments.size) + assertTrue(sentMessage.attachments.first().data::class == AttachmentBase64::class) + val json = sentMessage.attachments.first().data.getDataAsJsonString() + val presentationDefinition = Json.decodeFromString(json) + assertNotNull(presentationDefinition.presentationDefinition.format.sdjwt) + } + + @Test + fun `test handlePresentationDefinitionRequest for SD-JWT with no options, fails`() = runTest { + val agent = spy( + EdgeAgent( + apollo = apolloMock, + castor = castorMock, + pluto = plutoMock, + mercury = mercuryMock, + pollux = PolluxImpl(apolloMock, castorMock), + connectionManager = connectionManagerMock, + seed = seed, + api = null, + logger = LoggerMock() + ) + ) + + val privateKey = Secp256k1KeyPair.generateKeyPair( + seed = seed, + curve = KeyCurve(Curve.SECP256K1) + ).privateKey + val storablePrivateKeys = listOf( + StorablePrivateKey( + id = UUID.randomUUID().toString(), + restorationIdentifier = "secp256k1+priv", + data = privateKey.raw.base64UrlEncoded, + keyPathIndex = 0 + ) + ) + // Mock getDIDPrivateKeysByDID response + `when`(plutoMock.getDIDPrivateKeysByDID(any())).thenReturn(flow { emit(storablePrivateKeys) }) + `when`( + apolloMock.restorePrivateKey( + storablePrivateKeys.first().restorationIdentifier, + storablePrivateKeys.first().data + ) + ).thenReturn(privateKey) + + val credential = SDJWTCredential.fromSDJwtString( + "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJkaWQ6cHJpc206NmYyM2RkYWNlNTE5YjY4ZGZjMGZhMDZlOTkyZGI0MGYyZjNjNTg0YWYzODJjZTQ0NmZhMmZkMGUwNDJlNWRlYTpDb1VCQ29JQkVqc0tCMjFoYzNSbGNqQVFBVW91Q2dselpXTndNalUyYXpFU0lRTWNLd0VpdEdiUUt0R2EtakZYaTNtMXU3T1AySk11a1lYUW5aazNmUUlYdnhKRENnOWhkWFJvWlc1MGFXTmhkR2x2YmpBUUJFb3VDZ2x6WldOd01qVTJhekVTSVFNY0t3RWl0R2JRS3RHYS1qRlhpM20xdTdPUDJKTXVrWVhRblprM2ZRSVh2dyIsImVtYWlsQWRkcmVzcyI6eyJfc2QiOlsiVDFQbWo5XzVvSzl0UGQyX1h1MUI2bkpmMTR6UlpXWGdySmhnVkktbHV1OCJdfSwiX3NkX2FsZyI6InNoYS0yNTYiLCJpc3MiOiJkaWQ6cHJpc206Y2UzNDAzYjVhNzMzODgzMDM1ZDZlYzQzYmEwNzVhNDFjOWNjMGEzMjU3OTc3ZDgwYzc1ZDYzMTlhZGUwZWQ3MCIsImV4cCI6MTczNTY4OTY2MSwiaWF0IjoxNTE2MjM5MDIyfQ._sPrEKiMwJg5e6c24dgCXBH2FZ3tzZL-6llwGj9Rt_XQo1ABwJWB6Rdr_UO3ow3ooAdsIedy6Gau9NYbWv0fDw~WyJZMGlWS3BYYnBWdnpiang4Z2J4cUhnIiwiZW1haWxBZGRyZXNzIiwidGVzdEBpb2hrLmlvIl0~" + ) + + val msg = Json.decodeFromString( + """{"id":"56992a63-9871-490a-b9f8-4b1238c23c5e","piuri":"https://didcomm.atalaprism.io/present-proof/3.0/request-presentation","from":{"method":"peer","methodId":"asdf"},"to":{"method":"peer","methodId":"fdsafdsa"},"fromPrior":null,"body":"{\"proof_types\":[]}","created_time":"1726767099","expires_time_plus":"1726853499","attachments":[{"id":"f135525e-26c7-44f5-8f23-b8fbc928bfb2","media_type":"application/json","data":{"base64":"eyJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IjI0YTdlNWU4LWQ3YjQtNDUxYy1hOThkLTA3ZDY4NjVhMzQwYSIsImlucHV0X2Rlc2NyaXB0b3JzIjpbeyJpZCI6IjNmMWYzYTliLThjZDUtNDZkMS04Y2E0LTBlYzYyN2YxZTdmMiIsIm5hbWUiOiJQcmVzZW50YXRpb24iLCJwdXJwb3NlIjoiUHJlc2VudGF0aW9uIGRlZmluaXRpb24iLCJmb3JtYXQiOnsic2RKd3QiOnsiYWxnIjpbIkVTMjU2ayJdfX0sImNvbnN0cmFpbnRzIjp7ImZpZWxkcyI6W3sicGF0aCI6WyIkLnZjLmNyZWRlbnRpYWxTdWJqZWN0LmZhbWlseU5hbWUiLCIkLmNyZWRlbnRpYWxTdWJqZWN0LmZhbWlseU5hbWUiXSwiaWQiOiI5MjM3ZmNhMy1lZjcyLTQyOGEtYjIyYy02YzVmZTZmZTU0NWIiLCJuYW1lIjoiZmFtaWx5TmFtZSIsImZpbHRlciI6eyJ0eXBlIjoic3RyaW5nIiwicGF0dGVybiI6IldvbmRlcmxhbmQifX0seyJwYXRoIjpbIiQudmMuY3JlZGVudGlhbFN1YmplY3QuZ2l2ZW5OYW1lIiwiJC5jcmVkZW50aWFsU3ViamVjdC5naXZlbk5hbWUiXSwiaWQiOiI3MmQ4NmYwMS04NGYzLTRhNDYtOGFiOC1hN2I0OGE5YjU2MTAiLCJuYW1lIjoiZ2l2ZW5OYW1lIiwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJwYXR0ZXJuIjoiQWxpY2UifX0seyJwYXRoIjpbIiQudmMuY3JlZGVudGlhbFN1YmplY3QuZHJpdmluZ0NsYXNzIiwiJC5jcmVkZW50aWFsU3ViamVjdC5kcml2aW5nQ2xhc3MiXSwiaWQiOiIyNmI3ZmY3Zi1kOTcyLTQxZGYtYTNkZC0zYmE3YzhiNDAwOWEiLCJuYW1lIjoiZHJpdmluZ0NsYXNzIiwiZmlsdGVyIjp7InR5cGUiOiJpbnRlZ2VyIiwicGF0dGVybiI6IjMifX0seyJwYXRoIjpbIiQudmMuY3JlZGVudGlhbFN1YmplY3QuZGF0ZU9mSXNzdWFuY2UiLCIkLmNyZWRlbnRpYWxTdWJqZWN0LmRhdGVPZklzc3VhbmNlIl0sImlkIjoiYWZiZmI1NWMtMWY5Ni00ODlkLWJmOGUtYzZhNTUxYjg3ODFjIiwibmFtZSI6ImRhdGVPZklzc3VhbmNlIiwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJwYXR0ZXJuIjoiMjAyMC0xMS0xM1QyMDoyMDozOSswMDowMCJ9fSx7InBhdGgiOlsiJC52Yy5jcmVkZW50aWFsU3ViamVjdC5lbWFpbEFkZHJlc3MiLCIkLmNyZWRlbnRpYWxTdWJqZWN0LmVtYWlsQWRkcmVzcyJdLCJpZCI6IjAyZGIxOTA0LWY3NTMtNDI2NC04ZTA0LWI5NGMxNWFkYzA3MyIsIm5hbWUiOiJlbWFpbEFkZHJlc3MiLCJmaWx0ZXIiOnsidHlwZSI6InN0cmluZyIsInBhdHRlcm4iOiJhbGljZUB3b25kZXJsYW5kLmNvbSJ9fSx7InBhdGgiOlsiJC52Yy5jcmVkZW50aWFsU3ViamVjdC5kcml2aW5nTGljZW5zZUlEIiwiJC5jcmVkZW50aWFsU3ViamVjdC5kcml2aW5nTGljZW5zZUlEIl0sImlkIjoiODgzYThkMzYtNWYzMy00OGVjLWJhYjktNzE0MGRiYWMyYTVmIiwibmFtZSI6ImRyaXZpbmdMaWNlbnNlSUQiLCJmaWx0ZXIiOnsidHlwZSI6InN0cmluZyIsInBhdHRlcm4iOiIxMjM0NSJ9fV0sImxpbWl0X2Rpc2Nsb3N1cmUiOiJyZXF1aXJlZCJ9fV0sImZvcm1hdCI6eyJzZEp3dCI6eyJhbGciOlsiRVMyNTZrIl19fX0sIm9wdGlvbnMiOnsicHJlc2VudGF0aW9uRnJhbWUiOnt9fX0"},"format":"dif/presentation-exchange/definitions@v1.0"}],"thid":"11001ef6-f4c9-430c-84d7-ef74f0689e9f","ack":[],"direction":"SENT"}""" + ) + + assertFailsWith(EdgeAgentError.MissingOrNullFieldError::class) { + agent.preparePresentationForRequestProof( + request = RequestPresentation.fromMessage(msg), + credential = credential + ) + } + } + + @Test + fun `test handlePresentationDefinitionRequest for SD-JWT`() = runTest { + val agent = spy( + EdgeAgent( + apollo = apolloMock, + castor = castorMock, + pluto = plutoMock, + mercury = mercuryMock, + pollux = PolluxImpl(apolloMock, castorMock), + connectionManager = connectionManagerMock, + seed = seed, + api = null, + logger = LoggerMock() + ) + ) + + val privateKey = Secp256k1KeyPair.generateKeyPair( + seed = seed, + curve = KeyCurve(Curve.SECP256K1) + ).privateKey + val storablePrivateKeys = listOf( + StorablePrivateKey( + id = UUID.randomUUID().toString(), + restorationIdentifier = "secp256k1+priv", + data = privateKey.raw.base64UrlEncoded, + keyPathIndex = 0 + ) + ) + // Mock getDIDPrivateKeysByDID response + `when`(plutoMock.getDIDPrivateKeysByDID(any())).thenReturn(flow { emit(storablePrivateKeys) }) + `when`( + apolloMock.restorePrivateKey( + storablePrivateKeys.first().restorationIdentifier, + storablePrivateKeys.first().data + ) + ).thenReturn(privateKey) + + `when`(plutoMock.getPrismLastKeyPathIndex()).thenReturn(flow { emit(0) }) + + val credential = SDJWTCredential.fromSDJwtString( + "eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJkaWQ6cHJpc206MGE0YjU1MjE2OWUzMTU4NzgxNzQxZmJiZWZmZTgxMjEyNzg0ZDMyZDkwY2Y4ZjI2MjI5MjNmMTFmNmVjZDk2NjpDb1VCQ29JQkVqc0tCMjFoYzNSbGNqQVFBVW91Q2dselpXTndNalUyYXpFU0lRTGd6aHN1T3FoQXlJbXktYzhvOVptSUo0aVlfR2M4dHZOSVQzbDF3NThmMkJKRENnOWhkWFJvWlc1MGFXTmhkR2x2YmpBUUJFb3VDZ2x6WldOd01qVTJhekVTSVFMZ3poc3VPcWhBeUlteS1jOG85Wm1JSjRpWV9HYzh0dk5JVDNsMXc1OGYyQSIsIl9zZCI6WyJXVTFWMTZWbTdnd3R3SHRrRGVsdGF1VjlUNTl2SW4yZW9UQVdnU3dtdnQ0Il0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiZGlkOnByaXNtOmNlMzQwM2I1YTczMzg4MzAzNWQ2ZWM0M2JhMDc1YTQxYzljYzBhMzI1Nzk3N2Q4MGM3NWQ2MzE5YWRlMGVkNzAiLCJleHAiOjE3MzU2ODk2NjEsImlhdCI6MTUxNjIzOTAyMn0.ITRzKaRaY5GB5zIlgP1KdLYtthnededjXZFc9MJTjKCIcSgdyEBNJxPXHftAoC9I9db3GZ6r_Psmq7S_0f9NAA~WyI1M1R1MlN2bmM3OThaUnhHaEE2b0VRIiwiZW1haWxBZGRyZXNzIiwidGVzdEBpb2hrLmlvIl0~" + ) + + val msg = Json.decodeFromString( + """{"id":"56992a63-9871-490a-b9f8-4b1238c23c5e","piuri":"https://didcomm.atalaprism.io/present-proof/3.0/request-presentation","from":{"method":"peer","methodId":"asdf"},"to":{"method":"peer","methodId":"fdsafdsa"},"fromPrior":null,"body":"{\"proof_types\":[]}","created_time":"1726767099","expires_time_plus":"1726853499","attachments":[{"id":"f135525e-26c7-44f5-8f23-b8fbc928bfb2","media_type":"application/json","data":{"base64":"eyJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IjI0YTdlNWU4LWQ3YjQtNDUxYy1hOThkLTA3ZDY4NjVhMzQwYSIsImlucHV0X2Rlc2NyaXB0b3JzIjpbeyJpZCI6IjNmMWYzYTliLThjZDUtNDZkMS04Y2E0LTBlYzYyN2YxZTdmMiIsIm5hbWUiOiJQcmVzZW50YXRpb24iLCJwdXJwb3NlIjoiUHJlc2VudGF0aW9uIGRlZmluaXRpb24iLCJmb3JtYXQiOnsic2RKd3QiOnsiYWxnIjpbIkVTMjU2ayJdfX0sImNvbnN0cmFpbnRzIjp7ImZpZWxkcyI6W3sicGF0aCI6WyIkLnZjLmNyZWRlbnRpYWxTdWJqZWN0LmZhbWlseU5hbWUiLCIkLmNyZWRlbnRpYWxTdWJqZWN0LmZhbWlseU5hbWUiXSwiaWQiOiI5MjM3ZmNhMy1lZjcyLTQyOGEtYjIyYy02YzVmZTZmZTU0NWIiLCJuYW1lIjoiZmFtaWx5TmFtZSIsImZpbHRlciI6eyJ0eXBlIjoic3RyaW5nIiwicGF0dGVybiI6IldvbmRlcmxhbmQifX0seyJwYXRoIjpbIiQudmMuY3JlZGVudGlhbFN1YmplY3QuZ2l2ZW5OYW1lIiwiJC5jcmVkZW50aWFsU3ViamVjdC5naXZlbk5hbWUiXSwiaWQiOiI3MmQ4NmYwMS04NGYzLTRhNDYtOGFiOC1hN2I0OGE5YjU2MTAiLCJuYW1lIjoiZ2l2ZW5OYW1lIiwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJwYXR0ZXJuIjoiQWxpY2UifX0seyJwYXRoIjpbIiQudmMuY3JlZGVudGlhbFN1YmplY3QuZHJpdmluZ0NsYXNzIiwiJC5jcmVkZW50aWFsU3ViamVjdC5kcml2aW5nQ2xhc3MiXSwiaWQiOiIyNmI3ZmY3Zi1kOTcyLTQxZGYtYTNkZC0zYmE3YzhiNDAwOWEiLCJuYW1lIjoiZHJpdmluZ0NsYXNzIiwiZmlsdGVyIjp7InR5cGUiOiJpbnRlZ2VyIiwicGF0dGVybiI6IjMifX0seyJwYXRoIjpbIiQudmMuY3JlZGVudGlhbFN1YmplY3QuZGF0ZU9mSXNzdWFuY2UiLCIkLmNyZWRlbnRpYWxTdWJqZWN0LmRhdGVPZklzc3VhbmNlIl0sImlkIjoiYWZiZmI1NWMtMWY5Ni00ODlkLWJmOGUtYzZhNTUxYjg3ODFjIiwibmFtZSI6ImRhdGVPZklzc3VhbmNlIiwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJwYXR0ZXJuIjoiMjAyMC0xMS0xM1QyMDoyMDozOSswMDowMCJ9fSx7InBhdGgiOlsiJC52Yy5jcmVkZW50aWFsU3ViamVjdC5lbWFpbEFkZHJlc3MiLCIkLmNyZWRlbnRpYWxTdWJqZWN0LmVtYWlsQWRkcmVzcyJdLCJpZCI6IjAyZGIxOTA0LWY3NTMtNDI2NC04ZTA0LWI5NGMxNWFkYzA3MyIsIm5hbWUiOiJlbWFpbEFkZHJlc3MiLCJmaWx0ZXIiOnsidHlwZSI6InN0cmluZyIsInBhdHRlcm4iOiJhbGljZUB3b25kZXJsYW5kLmNvbSJ9fSx7InBhdGgiOlsiJC52Yy5jcmVkZW50aWFsU3ViamVjdC5kcml2aW5nTGljZW5zZUlEIiwiJC5jcmVkZW50aWFsU3ViamVjdC5kcml2aW5nTGljZW5zZUlEIl0sImlkIjoiODgzYThkMzYtNWYzMy00OGVjLWJhYjktNzE0MGRiYWMyYTVmIiwibmFtZSI6ImRyaXZpbmdMaWNlbnNlSUQiLCJmaWx0ZXIiOnsidHlwZSI6InN0cmluZyIsInBhdHRlcm4iOiIxMjM0NSJ9fV0sImxpbWl0X2Rpc2Nsb3N1cmUiOiJyZXF1aXJlZCJ9fV0sImZvcm1hdCI6eyJzZEp3dCI6eyJhbGciOlsiRVMyNTZrIl19fX19"},"format":"dif/presentation-exchange/definitions@v1.0"}],"thid":"11001ef6-f4c9-430c-84d7-ef74f0689e9f","ack":[],"direction":"SENT"}""" + ) + + val presentation = agent.preparePresentationForRequestProof( + request = RequestPresentation.fromMessage(msg), + credential = credential, + preparePresentationOptions = SDJWTPreparePresentationOptions( + presentationFrame = mapOf("/emailAddress" to true) + ) + ) + + assertEquals(1, presentation.attachments.size) + val attachmentDescriptor = presentation.attachments.first() + val attachmentData = attachmentDescriptor.data + assertTrue(attachmentData is AttachmentBase64) + val jwt = attachmentData.getDataAsJsonString() + assertTrue(jwt.contains(".")) + } + @Test fun `test connectionless credential offer correctly`() = runTest { val agent = spy( diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/PlutoMock.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/PlutoMock.kt index 384c83761..276468fb2 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/PlutoMock.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/edgeagent/PlutoMock.kt @@ -246,10 +246,6 @@ class PlutoMock : Pluto { override fun storeLinkSecret(linkSecret: String) { } - override fun storeCredentialMetadata(name: String, metadata: CredentialRequestMeta) { - TODO("Not yet implemented") - } - override fun storeCredentialMetadata(name: String, linkSecretName: String, json: String) { TODO("Not yet implemented") } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/mercury/CastorMock.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/mercury/CastorMock.kt index bd5e4c773..f6cce338a 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/mercury/CastorMock.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/mercury/CastorMock.kt @@ -27,7 +27,8 @@ class CastorMock : Castor { @Throws(Exception::class) override fun createPrismDID( masterPublicKey: PublicKey, - services: Array? + services: Array?, + authenticationKeys: Array ): DID { return createPrismDIDReturn ?: throw Exception("createPrismDID() not implemented in mock") } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/mercury/PlutoMock.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/mercury/PlutoMock.kt index 0b0223071..51f89fb4c 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/mercury/PlutoMock.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/mercury/PlutoMock.kt @@ -195,10 +195,6 @@ class PlutoMock : Pluto { TODO("Not yet implemented") } - override fun storeCredentialMetadata(name: String, metadata: CredentialRequestMeta) { - TODO("Not yet implemented") - } - override fun storeCredentialMetadata(name: String, linkSecretName: String, json: String) { TODO("Not yet implemented") } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pluto/BackupRestorationTests.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pluto/BackupRestorationTests.kt index ad678b453..b46b5ea03 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pluto/BackupRestorationTests.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pluto/BackupRestorationTests.kt @@ -36,6 +36,7 @@ import org.hyperledger.identus.walletsdk.logger.LoggerMock import org.hyperledger.identus.walletsdk.pluto.models.backup.BackupV0_0_1 import org.hyperledger.identus.walletsdk.pollux.PolluxImpl import org.hyperledger.identus.walletsdk.pollux.models.JWTCredential +import org.hyperledger.identus.walletsdk.pollux.models.SDJWTCredential import org.junit.Before import org.junit.Test import org.mockito.Mock @@ -219,6 +220,11 @@ class BackupRestorationTests { JWTCredential.fromJwtString(jwtString).toStorableCredential() } + RestorationID.SDJWT -> { + val jwtString = it.credentialData.base64UrlDecoded + SDJWTCredential.fromSDJwtString(jwtString).toStorableCredential() + } + RestorationID.ANONCRED -> { val data = it.credentialData.base64UrlDecodedBytes PlutoRestoreTask.AnonCredentialBackUp.fromStorableData(data) diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/CastorMock.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/CastorMock.kt index 2e6024c88..762fbe3b4 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/CastorMock.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/CastorMock.kt @@ -26,7 +26,8 @@ class CastorMock : Castor { override fun createPrismDID( masterPublicKey: PublicKey, - services: Array? + services: Array?, + authenticationKeys: Array ): DID { TODO("Not yet implemented") } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/DescriptorPathTest.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/DescriptorPathTest.kt new file mode 100644 index 000000000..400911e62 --- /dev/null +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/DescriptorPathTest.kt @@ -0,0 +1,37 @@ +package org.hyperledger.identus.walletsdk.pollux + +import junit.framework.TestCase.assertTrue +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.encodeToJsonElement +import org.hyperledger.identus.walletsdk.pollux.models.PresentationSubmission +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +class DescriptorPathTest { + + @Test + fun testGetValue_whenDollarSign_thenValueFound() { + val submission = """{"presentation_submission":{"id":"3afa67f4-5181-4cec-a296-0f52705126a9","definition_id":"6ceedd50-187d-4731-b779-c7e1bf771191","descriptor_map":[{"id":"a319a6b7-b8b0-441f-a1ca-ed454d151ee5","format":"jwt_vp","path":"${'$'}.verifiablePresentation[0]","path_nested":{"id":"a319a6b7-b8b0-441f-a1ca-ed454d151ee5","format":"jwt_vc","path":"${'$'}.vp.verifiableCredential[0]"}}]},"verifiablePresentation":["eyJraWQiOiJkaWQ6cHJpc206Y2JjNmQyOGE1NTI0NWZhZGYwYmU4ZmU5NDZhY2Q2ZjRiZjJkN2E4MWVlMTYzZjVlM2U0M2YwOTM2NWM2NWIwZjpDb1VCQ29JQkVqc0tCMjFoYzNSbGNqQVFBVW91Q2dselpXTndNalUyYXpFU0lRUHVRTG4zaC1IMDlMN0lQYkhZSy0xNTZKdC1ScWlydUdWdlgwTzJ0VVRvNEJKRENnOWhkWFJvWlc1MGFXTmhkR2x2YmpBUUJFb3VDZ2x6WldOd01qVTJhekVTSVFQdVFMbjNoLUgwOUw3SVBiSFlLLTE1Nkp0LVJxaXJ1R1Z2WDBPMnRVVG80QSNhdXRoZW50aWNhdGlvbjAiLCJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206Y2JjNmQyOGE1NTI0NWZhZGYwYmU4ZmU5NDZhY2Q2ZjRiZjJkN2E4MWVlMTYzZjVlM2U0M2YwOTM2NWM2NWIwZjpDb1VCQ29JQkVqc0tCMjFoYzNSbGNqQVFBVW91Q2dselpXTndNalUyYXpFU0lRUHVRTG4zaC1IMDlMN0lQYkhZSy0xNTZKdC1ScWlydUdWdlgwTzJ0VVRvNEJKRENnOWhkWFJvWlc1MGFXTmhkR2x2YmpBUUJFb3VDZ2x6WldOd01qVTJhekVTSVFQdVFMbjNoLUgwOUw3SVBiSFlLLTE1Nkp0LVJxaXJ1R1Z2WDBPMnRVVG80QSIsImF1ZCI6ImRvbWFpbiIsInZwIjp7IkBjb250ZXh0IjpbImh0dHBzOlwvXC93d3cudzMub3JnXC8yMDE4XC9jcmVkZW50aWFsc1wvdjEiXSwidHlwZSI6WyJWZXJpZmlhYmxlUHJlc2VudGF0aW9uIl0sInZlcmlmaWFibGVDcmVkZW50aWFsIjpbImV5SmhiR2NpT2lKRlV6STFOa3NpZlEuZXlKaGRXUWlPaUpoTVdVME1UVTFNQzAxWkRVMExUUmhOelF0T1RBeVppMWtOV1psTWpZeVpHRXhPVEFpTENKemRXSWlPaUprYVdRNmNISnBjMjA2WTJKak5tUXlPR0UxTlRJME5XWmhaR1l3WW1VNFptVTVORFpoWTJRMlpqUmlaakprTjJFNE1XVmxNVFl6WmpWbE0yVTBNMll3T1RNMk5XTTJOV0l3WmpwRGIxVkNRMjlKUWtWcWMwdENNakZvWXpOU2JHTnFRVkZCVlc5MVEyZHNlbHBYVG5kTmFsVXlZWHBGVTBsUlVIVlJURzR6YUMxSU1EbE1OMGxRWWtoWlN5MHhOVFpLZEMxU2NXbHlkVWRXZGxnd1R6SjBWVlJ2TkVKS1JFTm5PV2hrV0ZKdldsYzFNR0ZYVG1oa1IyeDJZbXBCVVVKRmIzVkRaMng2V2xkT2QwMXFWVEpoZWtWVFNWRlFkVkZNYmpOb0xVZ3dPVXczU1ZCaVNGbExMVEUxTmtwMExWSnhhWEoxUjFaMldEQlBNblJWVkc4MFFTSXNJbTVpWmlJNk1UY3lOekl3TXpVNE15d2lhWE56SWpvaVpHbGtPbkJ5YVhOdE9tTXdNamMwWkdObFltSTFNR0l5TlRFM05XTmxPVFEzTldNM05EZGhaamxpWW1WaVlUZGhabUptTWpFMU9UUTJNRFkzTVRjd1lXVTFaamxpWWpoaU1EazZRMjlWUWtOdlNVSkZhbk5MUWpJeGFHTXpVbXhqYWtGUlFWVnZkVU5uYkhwYVYwNTNUV3BWTW1GNlJWTkpVVXR5WjA1eVdXTTNWVmt5TVRFeGJsbHRObnBEV201bGRqZDVhRFZPY0ZFd09YQnpVM2c0ZEVjMGRYbFNTa1JEWnpsb1pGaFNiMXBYTlRCaFYwNW9aRWRzZG1KcVFWRkNSVzkxUTJkc2VscFhUbmROYWxVeVlYcEZVMGxSUzNKblRuSlpZemRWV1RJeE1URnVXVzAyZWtOYWJtVjJOM2xvTlU1d1VUQTVjSE5UZURoMFJ6UjFlVkVpTENKbGVIQWlPakUzTWprM09UVTFPRE1zSW5aaklqcDdJbU55WldSbGJuUnBZV3hUWTJobGJXRWlPbTUxYkd3c0luUmxjbTF6VDJaVmMyVWlPbTUxYkd3c0ltVjJhV1JsYm1ObElqcHVkV3hzTENKamNtVmtaVzUwYVdGc1UzVmlhbVZqZENJNmV5SmpiM1Z5YzJVaU9pSkpaR1Z1ZEhWeklGUnlZV2x1YVc1bklHTnZkWEp6WlNCRFpYSjBhV1pwWTJGMGFXOXVJREl3TWpRaWZTd2lZMjl1ZEdWNGRDSTZXeUpvZEhSd2N6cGNMMXd2ZDNkM0xuY3pMbTl5WjF3dk1qQXhPRnd2WTNKbFpHVnVkR2xoYkhOY0wzWXhJbDBzSW5KbFpuSmxjMmhUWlhKMmFXTmxJanB1ZFd4c0xDSjBlWEJsSWpwYklsWmxjbWxtYVdGaWJHVkRjbVZrWlc1MGFXRnNJbDBzSW1OeVpXUmxiblJwWVd4VGRHRjBkWE1pT25zaWMzUmhkSFZ6VUhWeWNHOXpaU0k2SWxKRlZrOURRVlJKVDA0aUxDSnpkR0YwZFhOTWFYTjBTVzVrWlhnaU9qSTFMQ0pwWkNJNkltaDBkSEE2WEM5Y0x6RXdMamt4TGpFd01DNHhNalk2T0RBd01Gd3ZjSEpwYzIwdFlXZGxiblJjTDJOeVpXUmxiblJwWVd3dGMzUmhkSFZ6WEM4MU1UUmxPRFV5T0MwMFlqTTRMVFEzTjJFdFlqQmxOQzB6TWpSaVltVXlNakEwTmpRak1qVWlMQ0owZVhCbElqb2lVM1JoZEhWelRHbHpkREl3TWpGRmJuUnllU0lzSW5OMFlYUjFjMHhwYzNSRGNtVmtaVzUwYVdGc0lqb2lhSFIwY0RwY0wxd3ZNVEF1T1RFdU1UQXdMakV5TmpvNE1EQXdYQzl3Y21semJTMWhaMlZ1ZEZ3dlkzSmxaR1Z1ZEdsaGJDMXpkR0YwZFhOY0x6VXhOR1U0TlRJNExUUmlNemd0TkRjM1lTMWlNR1UwTFRNeU5HSmlaVEl5TURRMk5DSjlmWDAuUFJnNkx0ZFV5TGNsQzlYbUVJLUxJWDQ4UWlzdVNfMDRCbDF3dHVXejNTamFHdXlSd0dRQ1ZxTldQUWFaZktNa2R2VUZ3bWtyMjZ4bE9fNDRlT2ZPeEEiXX0sIm5vbmNlIjoiMmYxODdjY2UtZDY1ZC00MGYzLTg2YzktYTk5ZTg3NzQ0ZGE3In0._UbbNfI9CUuUNgrLbECTw_20V-BSN-A4WmSuvj2lRbDiIkwCm5MJbbQrj8OiD85cJUEamW4QtQUr9-ckpE1aYg"]}""" + val descriptorPath = + DescriptorPath(Json.encodeToJsonElement(Json.decodeFromString(submission))) + val path = "\$.verifiablePresentation[0]" + val holderJws = descriptorPath.getValue(path) + assertNotNull(holderJws) + assertTrue(holderJws is String) + val path1 = "\$.verifiablePresentation" + val holderJws1 = descriptorPath.getValue(path1) + assertNotNull(holderJws1) + } + + @Test + fun testGetValue_whenNotDollarSign_thenValueFound() { + val claims = """{"sub":"did:prism:0a4b552169e3158781741fbbeffe81212784d32d90cf8f2622923f11f6ecd966:CoUBCoIBEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQLgzhsuOqhAyImy-c8o9ZmIJ4iY_Gc8tvNIT3l1w58f2BJDCg9hdXRoZW50aWNhdGlvbjAQBEouCglzZWNwMjU2azESIQLgzhsuOqhAyImy-c8o9ZmIJ4iY_Gc8tvNIT3l1w58f2A","iss":"did:prism:ce3403b5a733883035d6ec43ba075a41c9cc0a3257977d80c75d6319ade0ed70","exp":1735689661,"iat":1516239022,"emailAddress":"test@iohk.io"}""" + val descriptorPath = + DescriptorPath(Json.encodeToJsonElement(Json.parseToJsonElement(claims))) + val path = "/emailAddress" + val value = descriptorPath.getValue(path) + assertNotNull(value) + assertEquals("test@iohk.io", value) + } +} diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImplTest.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImplTest.kt index 037aec172..714c18359 100644 --- a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImplTest.kt +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/PolluxImplTest.kt @@ -13,6 +13,7 @@ import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.serialization.kotlinx.json.json +import java.security.interfaces.ECPrivateKey import java.text.SimpleDateFormat import java.util.* import junit.framework.TestCase.assertFalse @@ -35,6 +36,7 @@ import org.hyperledger.identus.walletsdk.domain.models.AnoncredsInputFieldFilter import org.hyperledger.identus.walletsdk.domain.models.AnoncredsPresentationClaims import org.hyperledger.identus.walletsdk.domain.models.Api import org.hyperledger.identus.walletsdk.domain.models.ApiImpl +import org.hyperledger.identus.walletsdk.domain.models.CredentialOperationsOptions import org.hyperledger.identus.walletsdk.domain.models.CredentialType import org.hyperledger.identus.walletsdk.domain.models.Curve import org.hyperledger.identus.walletsdk.domain.models.DID @@ -44,21 +46,25 @@ import org.hyperledger.identus.walletsdk.domain.models.JWTPresentationClaims import org.hyperledger.identus.walletsdk.domain.models.JWTVerifiableCredential import org.hyperledger.identus.walletsdk.domain.models.KeyCurve import org.hyperledger.identus.walletsdk.domain.models.KeyValue +import org.hyperledger.identus.walletsdk.domain.models.Message import org.hyperledger.identus.walletsdk.domain.models.PolluxError import org.hyperledger.identus.walletsdk.domain.models.PresentationClaims import org.hyperledger.identus.walletsdk.domain.models.RequestedAttributes +import org.hyperledger.identus.walletsdk.domain.models.SDJWTPresentationClaims import org.hyperledger.identus.walletsdk.domain.models.Seed import org.hyperledger.identus.walletsdk.domain.models.httpClient import org.hyperledger.identus.walletsdk.domain.models.keyManagement.PrivateKey import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.AnoncredsPresentationOptions import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.JWTPresentationOptions import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.PresentationSubmissionOptionsJWT +import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.PresentationSubmissionOptionsSDJWT +import org.hyperledger.identus.walletsdk.edgeagent.protocols.proofOfPresentation.SDJWTPresentationOptions import org.hyperledger.identus.walletsdk.logger.Logger -import org.hyperledger.identus.walletsdk.pollux.models.AnonCredential import org.hyperledger.identus.walletsdk.pollux.models.AnoncredsPresentationDefinitionRequest import org.hyperledger.identus.walletsdk.pollux.models.JWTCredential import org.hyperledger.identus.walletsdk.pollux.models.JWTPresentationDefinitionRequest import org.hyperledger.identus.walletsdk.pollux.models.PresentationSubmission +import org.hyperledger.identus.walletsdk.pollux.models.SDJWTPresentationDefinitionRequest import org.junit.Before import org.junit.Test import org.mockito.ArgumentMatchers.anyString @@ -197,7 +203,7 @@ class PolluxImplTest { definitionRequest.presentationDefinition.inputDescriptors.first().constraints.fields?.size ) assertEquals( - 2, + 3, definitionRequest.presentationDefinition.inputDescriptors.first().constraints.fields?.first()?.path?.size ) assertEquals( @@ -210,126 +216,6 @@ class PolluxImplTest { ) } - @Test - fun testCreatePresentationSubmission_whenCredentialNotJWT_thenExceptionThrown() = runTest { - val definitionJson = """ - { - "presentation_definition": { - "id": "32f54163-7166-48f1-93d8-ff217bdb0653", - "input_descriptors": [ - { - "id": "wa_driver_license", - "name": "Washington State Business License", - "purpose": "We can only allow licensed Washington State business representatives into the WA Business Conference", - "constraints": { - "fields": [ - { - "path": [ - "$.credentialSubject.dateOfBirth", - "$.credentialSubject.dob", - "$.vc.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dob" - ] - } - ] - } - } - ], - "format": { - "jwt": { - "alg": ["ES256K"] - } - } - }, - "options": { - "domain": "domain", - "challenge": "challenge" - } - } - """ - - val presentationDefinitionRequest = definitionJson - - val credential = AnonCredential( - schemaID = "", - credentialDefinitionID = "", - values = mapOf(), - signatureJson = "", - signatureCorrectnessProofJson = "", - revocationRegistryId = null, - revocationRegistryJson = null, - witnessJson = "", - json = "" - ) - val secpKeyPair = generateSecp256k1KeyPair() - - pollux = PolluxImpl(apollo, castor, api) - - assertFailsWith(PolluxError.CredentialTypeNotSupportedError::class) { - pollux.createJWTPresentationSubmission( - presentationDefinitionRequest = presentationDefinitionRequest, - credential = credential, - privateKey = secpKeyPair.privateKey - ) - } - } - - @Test - fun testCreatePresentationSubmission_whenPrivateKeyNotSecp256k1_thenExceptionThrown() = - runTest { - val definitionJson = """ - { - "presentation_definition": { - "id": "32f54163-7166-48f1-93d8-ff217bdb0653", - "input_descriptors": [ - { - "id": "wa_driver_license", - "name": "Washington State Business License", - "purpose": "We can only allow licensed Washington State business representatives into the WA Business Conference", - "constraints": { - "fields": [ - { - "path": [ - "$.credentialSubject.dateOfBirth", - "$.credentialSubject.dob", - "$.vc.credentialSubject.dateOfBirth", - "$.vc.credentialSubject.dob" - ] - } - ] - } - } - ], - "format": { - "jwt": { - "alg": ["ES256K"] - } - } - }, - "options": { - "domain": "domain", - "challenge": "challenge" - } - } - """ - - val presentationDefinitionRequest = definitionJson - val credential = JWTCredential.fromJwtString( - "eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206MjU3MTlhOTZiMTUxMjA3MTY5ODFhODQzMGFkMGNiOTY4ZGQ1MzQwNzM1OTNjOGNkM2YxZDI3YTY4MDRlYzUwZTpDcG9DQ3BjQ0Vsb0tCV3RsZVMweEVBSkNUd29KYzJWamNESTFObXN4RWlBRW9TQ241dHlEYTZZNnItSW1TcXBKOFkxbWo3SkMzX29VekUwTnl5RWlDQm9nc2dOYWVSZGNDUkdQbGU4MlZ2OXRKZk53bDZyZzZWY2hSM09xaGlWYlRhOFNXd29HWVhWMGFDMHhFQVJDVHdvSmMyVmpjREkxTm1zeEVpRE1rQmQ2RnRpb0prM1hPRnUtX2N5NVhtUi00dFVRMk5MR2lXOGFJU29ta1JvZzZTZGU5UHduRzBRMFNCVG1GU1REYlNLQnZJVjZDVExYcmpJSnR0ZUdJbUFTWEFvSGJXRnpkR1Z5TUJBQlFrOEtDWE5sWTNBeU5UWnJNUklnTzcxMG10MVdfaXhEeVFNM3hJczdUcGpMQ05PRFF4Z1ZoeDVzaGZLTlgxb2FJSFdQcnc3SVVLbGZpYlF0eDZKazRUU2pnY1dOT2ZjT3RVOUQ5UHVaN1Q5dCIsInN1YiI6ImRpZDpwcmlzbTpiZWVhNTIzNGFmNDY4MDQ3MTRkOGVhOGVjNzdiNjZjYzdmM2U4MTVjNjhhYmI0NzVmMjU0Y2Y5YzMwNjI2NzYzOkNzY0JDc1FCRW1RS0QyRjFkR2hsYm5ScFkyRjBhVzl1TUJBRVFrOEtDWE5sWTNBeU5UWnJNUklnZVNnLTJPTzFKZG5welVPQml0eklpY1hkZnplQWNUZldBTi1ZQ2V1Q2J5SWFJSlE0R1RJMzB0YVZpd2NoVDNlMG5MWEJTNDNCNGo5amxzbEtvMlpsZFh6akVsd0tCMjFoYzNSbGNqQVFBVUpQQ2dselpXTndNalUyYXpFU0lIa29QdGpqdFNYWjZjMURnWXJjeUluRjNYODNnSEUzMWdEZm1BbnJnbThpR2lDVU9Ca3lOOUxXbFlzSElVOTN0Snkxd1V1TndlSV9ZNWJKU3FObVpYVjg0dyIsIm5iZiI6MTY4NTYzMTk5NSwiZXhwIjoxNjg1NjM1NTk1LCJ2YyI6eyJjcmVkZW50aWFsU3ViamVjdCI6eyJhZGRpdGlvbmFsUHJvcDIiOiJUZXN0MyIsImlkIjoiZGlkOnByaXNtOmJlZWE1MjM0YWY0NjgwNDcxNGQ4ZWE4ZWM3N2I2NmNjN2YzZTgxNWM2OGFiYjQ3NWYyNTRjZjljMzA2MjY3NjM6Q3NjQkNzUUJFbVFLRDJGMWRHaGxiblJwWTJGMGFXOXVNQkFFUWs4S0NYTmxZM0F5TlRack1SSWdlU2ctMk9PMUpkbnB6VU9CaXR6SWljWGRmemVBY1RmV0FOLVlDZXVDYnlJYUlKUTRHVEkzMHRhVml3Y2hUM2UwbkxYQlM0M0I0ajlqbHNsS28yWmxkWHpqRWx3S0IyMWhjM1JsY2pBUUFVSlBDZ2x6WldOd01qVTJhekVTSUhrb1B0amp0U1haNmMxRGdZcmN5SW5GM1g4M2dIRTMxZ0RmbUFucmdtOGlHaUNVT0JreU45TFdsWXNISVU5M3RKeTF3VXVOd2VJX1k1YkpTcU5tWlhWODR3In0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiXSwiQGNvbnRleHQiOlsiaHR0cHM6XC9cL3d3dy53My5vcmdcLzIwMThcL2NyZWRlbnRpYWxzXC92MSJdfX0.x0SF17Y0VCDmt7HceOdTxfHlofsZmY18Rn6VQb0-r-k_Bm3hTi1-k2vkdjB25hdxyTCvxam-AkAP-Ag3Ahn5Ng" - ) - val nonSecpKeyPair = Ed25519KeyPair.generateKeyPair() - - pollux = PolluxImpl(apollo, castor, api) - - assertFailsWith(PolluxError.PrivateKeyTypeNotSupportedError::class) { - pollux.createJWTPresentationSubmission( - presentationDefinitionRequest = presentationDefinitionRequest, - credential = credential, - privateKey = nonSecpKeyPair.privateKey - ) - } - } - @Test fun testCreatePresentationSubmission_whenAllCorrect_thenPresentationSubmissionProofWellFormed() = runTest { @@ -346,8 +232,16 @@ class PolluxImplTest { Seed(MnemonicHelper.createRandomSeed()), KeyCurve(Curve.SECP256K1) ) - val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray()) - val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray()) + val issuerDID = castor.createPrismDID( + issuerKeyPair.publicKey, + emptyArray(), + emptyArray() + ) + val holderDID = castor.createPrismDID( + holderKeyPair.publicKey, + emptyArray(), + emptyArray() + ) pollux = spy(PolluxImpl(apollo, castor, api)) @@ -402,8 +296,8 @@ class PolluxImplTest { Seed(MnemonicHelper.createRandomSeed()), KeyCurve(Curve.SECP256K1) ) - val issuerDID = castor.createPrismDID(wrongIssuerKeyPair.publicKey, emptyArray()) - val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray()) + val issuerDID = castor.createPrismDID(wrongIssuerKeyPair.publicKey, emptyArray(), emptyArray()) + val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray(), emptyArray()) val httpResponse = correctHttpResponseFetchRevocationRegistry() @@ -456,8 +350,8 @@ class PolluxImplTest { Seed(MnemonicHelper.createRandomSeed()), KeyCurve(Curve.SECP256K1) ) - val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray()) - val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray()) + val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray(), emptyArray()) + val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray(), emptyArray()) val httpResponse = correctHttpResponseFetchRevocationRegistry() @@ -512,8 +406,8 @@ class PolluxImplTest { Seed(MnemonicHelper.createRandomSeed()), KeyCurve(Curve.SECP256K1) ) - val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray()) - val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray()) + val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray(), emptyArray()) + val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray(), emptyArray()) val httpResponse = correctHttpResponseFetchRevocationRegistry() @@ -565,8 +459,8 @@ class PolluxImplTest { Seed(MnemonicHelper.createRandomSeed()), KeyCurve(Curve.SECP256K1) ) - val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray()) - val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray()) + val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray(), emptyArray()) + val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray(), emptyArray()) pollux = spy(PolluxImpl(apollo, castor, api)) @@ -612,8 +506,8 @@ class PolluxImplTest { Seed(MnemonicHelper.createRandomSeed()), KeyCurve(Curve.SECP256K1) ) - val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray()) - val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray()) + val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray(), emptyArray()) + val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray(), emptyArray()) val httpResponse = correctHttpResponseFetchRevocationRegistry() @@ -665,8 +559,8 @@ class PolluxImplTest { Seed(MnemonicHelper.createRandomSeed()), KeyCurve(Curve.SECP256K1) ) - val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray()) - val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray()) + val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray(), emptyArray()) + val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray(), emptyArray()) val httpResponse = correctHttpResponseFetchRevocationRegistry() @@ -729,8 +623,8 @@ class PolluxImplTest { Seed(MnemonicHelper.createRandomSeed()), KeyCurve(Curve.SECP256K1) ) - val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray()) - val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray()) + val issuerDID = castor.createPrismDID(issuerKeyPair.publicKey, emptyArray(), emptyArray()) + val holderDID = castor.createPrismDID(holderKeyPair.publicKey, emptyArray(), emptyArray()) pollux = spy(PolluxImpl(apollo, castor, api)) @@ -1182,7 +1076,112 @@ class PolluxImplTest { val json = Json.parseToJsonElement(header) assertTrue(json.jsonObject.containsKey("kid")) val kid = json.jsonObject["kid"]!!.jsonPrimitive.content - assertEquals("did:prism:cd6cf9f94a43c53e286b0f2015c0083701350a694f52a22ee02e3bd29d93eba9:CrQBCrEBEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xJDCg9hdXRoZW50aWNhdGlvbjAQBEouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xotCgojZGlkY29tbS0xEhBESURDb21tTWVzc2FnaW5nGg1kaWQ6cGVlcjp0ZXN0#authentication0", kid) + assertEquals( + "did:prism:cd6cf9f94a43c53e286b0f2015c0083701350a694f52a22ee02e3bd29d93eba9:CrQBCrEBEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xJDCg9hdXRoZW50aWNhdGlvbjAQBEouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xotCgojZGlkY29tbS0xEhBESURDb21tTWVzc2FnaW5nGg1kaWQ6cGVlcjp0ZXN0#authentication0", + kid + ) + } + + @Test + fun `Test signClaims for SD-JWT including kid`() = runTest { + pollux = PolluxImpl(apollo, castor, api) + + + val masterSk = Secp256k1KeyPair.generateKeyPair(); + val authenticationSk = Ed25519KeyPair.generateKeyPair() + + val did = castor.createPrismDID( + masterSk.publicKey, + arrayOf(), + arrayOf(authenticationSk.publicKey), + ) + + val domain = "domain" + val challenge = "challenge" + val credential = JWTCredential.fromJwtString( + "eyJhbGciOiJFUzI1NksifQ.eyJpc3MiOiJkaWQ6cHJpc206ZTAyZTgwOTlkNTAzNTEzNDVjNWRkODMxYTllOTExMmIzOTRhODVkMDA2NGEyZWI1OTQyOTA4MDczNGExNTliNjpDcmtCQ3JZQkVqb0tCbUYxZEdndE1SQUVTaTRLQ1hObFkzQXlOVFpyTVJJaEF1Vlljb3JmV25MMGZZdEE1dmdKSzRfLW9iM2JVRGMtdzJVT0hkTzNRRXZxRWpzS0IybHpjM1ZsTFRFUUFrb3VDZ2x6WldOd01qVTJhekVTSVFMQ3U5Tm50cXVwQmotME5DZE1BNzV6UmVCZXlhQ0pPMWFHWWVQNEJNUUhWQkk3Q2dkdFlYTjBaWEl3RUFGS0xnb0pjMlZqY0RJMU5tc3hFaUVET1dndlF4NnZSdTZ3VWI0RlljSnVhRUNqOUJqUE1KdlJwOEx3TTYxaEVUNCIsInN1YiI6ImRpZDpwcmlzbTpiZDgxZmY1NDQzNDJjMTAwNDZkZmE0YmEyOTVkNWIzNmU0Y2ZlNWE3ZWIxMjBlMTBlZTVjMjQ4NzAwNjUxMDA5OkNvVUJDb0lCRWpzS0IyMWhjM1JsY2pBUUFVb3VDZ2x6WldOd01qVTJhekVTSVFQdjVQNXl5Z3Jad2FKbFl6bDU5bTJIQURLVFhVTFBzUmUwa2dlRUh2dExnQkpEQ2c5aGRYUm9aVzUwYVdOaGRHbHZiakFRQkVvdUNnbHpaV053TWpVMmF6RVNJUVB2NVA1eXlnclp3YUpsWXpsNTltMkhBREtUWFVMUHNSZTBrZ2VFSHZ0TGdBIiwibmJmIjoxNzE1MDAwNjc0LCJleHAiOjE3MTg2MDA2NzQsInZjIjp7ImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImVtYWlsQWRkcmVzcyI6ImNyaXN0aWFuLmNhc3Ryb0Bpb2hrLmlvIiwiaWQiOiJkaWQ6cHJpc206YmQ4MWZmNTQ0MzQyYzEwMDQ2ZGZhNGJhMjk1ZDViMzZlNGNmZTVhN2ViMTIwZTEwZWU1YzI0ODcwMDY1MTAwOTpDb1VCQ29JQkVqc0tCMjFoYzNSbGNqQVFBVW91Q2dselpXTndNalUyYXpFU0lRUHY1UDV5eWdyWndhSmxZemw1OW0ySEFES1RYVUxQc1JlMGtnZUVIdnRMZ0JKRENnOWhkWFJvWlc1MGFXTmhkR2x2YmpBUUJFb3VDZ2x6WldOd01qVTJhekVTSVFQdjVQNXl5Z3Jad2FKbFl6bDU5bTJIQURLVFhVTFBzUmUwa2dlRUh2dExnQSJ9LCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIl0sIkBjb250ZXh0IjpbImh0dHBzOlwvXC93d3cudzMub3JnXC8yMDE4XC9jcmVkZW50aWFsc1wvdjEiXSwiY3JlZGVudGlhbFN0YXR1cyI6eyJzdGF0dXNQdXJwb3NlIjoiUmV2b2NhdGlvbiIsInN0YXR1c0xpc3RJbmRleCI6MjUsImlkIjoiaHR0cDpcL1wvMTAuOTEuMTAwLjEyNjo4MDAwXC9wcmlzbS1hZ2VudFwvY3JlZGVudGlhbC1zdGF0dXNcLzUxNGU4NTI4LTRiMzgtNDc3YS1iMGU0LTMyNGJiZTIyMDQ2NCMyNSIsInR5cGUiOiJTdGF0dXNMaXN0MjAyMUVudHJ5Iiwic3RhdHVzTGlzdENyZWRlbnRpYWwiOiJodHRwOlwvXC8xMC45MS4xMDAuMTI2OjgwMDBcL3ByaXNtLWFnZW50XC9jcmVkZW50aWFsLXN0YXR1c1wvNTE0ZTg1MjgtNGIzOC00NzdhLWIwZTQtMzI0YmJlMjIwNDY0In19fQ.5OmmL5tdcRKugiHVt01PJUhp9r22zgMPPALUOB41g_1_BPHE3ezqJ2QhSmScU_EOGYcKksHftdrvfoND65nSjw" + ) + val signedClaims = pollux.signClaims( + subjectDID = did, + privateKey = authenticationSk.privateKey, + domain = domain, + challenge = challenge, + credential = credential + ) + assertTrue(signedClaims.contains(".")) + val splits = signedClaims.split(".") + val header = splits[0].base64UrlDecoded + val json = Json.parseToJsonElement(header) + assertTrue(json.jsonObject.containsKey("kid")) + val kid = json.jsonObject["kid"]!!.jsonPrimitive.content + assertEquals( + "did:prism:cd6cf9f94a43c53e286b0f2015c0083701350a694f52a22ee02e3bd29d93eba9:CrQBCrEBEjsKB21hc3RlcjAQAUouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xJDCg9hdXRoZW50aWNhdGlvbjAQBEouCglzZWNwMjU2azESIQKJIokEe_iKRGsr0f2EEa1JHGm59g0qP7QMtw6FcVxW9xotCgojZGlkY29tbS0xEhBESURDb21tTWVzc2FnaW5nGg1kaWQ6cGVlcjp0ZXN0#authentication0", + kid + ) + } + + @Test + fun `Test SD-JWT createPresentationDefinitionRequest`() = runTest { + pollux = PolluxImpl(apollo, castor, api) + + val sdJwtPresentationDefinitionRequest = pollux.createPresentationDefinitionRequest( + type = CredentialType.SDJWT, + presentationClaims = SDJWTPresentationClaims( + claims = mapOf( + "first_name" to InputFieldFilter( + type = "string", + pattern = "Wonderland" + ), + "last_name" to InputFieldFilter( + type = "string", + pattern = "Alice" + ), + "emailAddress" to InputFieldFilter( + type = "string", + pattern = "alice@wonderland.com" + ), + ) + ), + options = SDJWTPresentationOptions() + ) + val presentationDefinition = + Json.decodeFromString(sdJwtPresentationDefinitionRequest) + val inputDescriptor = presentationDefinition.presentationDefinition.inputDescriptors.first() + assertNotNull(inputDescriptor.constraints.fields) + var firstNameAvailable = false + var lastNameAvailable = false + var emailAvailable = false + inputDescriptor.constraints.fields!!.forEach { + if (it.name == "first_name") { + firstNameAvailable = true + } + if (it.name == "last_name") { + lastNameAvailable = true + } + if (it.name == "emailAddress") { + emailAvailable = true + } + } + assertTrue(firstNameAvailable && lastNameAvailable && emailAvailable) + } + + @Test + fun `test verifyPresentationSubmission SD-JWT`() = runTest { + pollux = PolluxImpl(apollo, castor, api) + val msg = Json.decodeFromString( + """{"id":"56992a63-9871-490a-b9f8-4b1238c23c5e","piuri":"https://didcomm.atalaprism.io/present-proof/3.0/request-presentation","from":{"method":"peer","methodId":"asdf"},"to":{"method":"peer","methodId":"fdsafdsa"},"fromPrior":null,"body":"{\"proof_types\":[]}","created_time":"1726767099","expires_time_plus":"1726853499","attachments":[{"id":"f135525e-26c7-44f5-8f23-b8fbc928bfb2","media_type":"application/json","data":{"base64":"eyJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IjVhMTljNjZmLWEwZTUtNGFhOC1iNmE5LTdjZGIzYzk0ZGI0NyIsImlucHV0X2Rlc2NyaXB0b3JzIjpbeyJpZCI6IjBjNGY1ZDg0LTBmYjItNGU0Yy1iMDViLTZjZTBmMGFmYzU4NiIsIm5hbWUiOiJQcmVzZW50YXRpb24iLCJwdXJwb3NlIjoiUHJlc2VudGF0aW9uIGRlZmluaXRpb24iLCJmb3JtYXQiOnsic2RKd3QiOnsiYWxnIjpbIkVTMjU2SyJdfX0sImNvbnN0cmFpbnRzIjp7ImZpZWxkcyI6W3sicGF0aCI6WyIkLnZjLmNyZWRlbnRpYWxTdWJqZWN0LmZpcnN0X25hbWUiLCIkLmNyZWRlbnRpYWxTdWJqZWN0LmZpcnN0X25hbWUiLCIkLmZpcnN0X25hbWUiXSwiaWQiOiI2YWFmYmI3NC01ZjE4LTQ4YTMtOGJlNC00MmI3NWIzZmNlZDgiLCJuYW1lIjoiZmlyc3RfbmFtZSIsImZpbHRlciI6eyJ0eXBlIjoic3RyaW5nIiwicGF0dGVybiI6IkNyaXN0aWFuIn19LHsicGF0aCI6WyIkLnZjLmNyZWRlbnRpYWxTdWJqZWN0Lmxhc3RfbmFtZSIsIiQuY3JlZGVudGlhbFN1YmplY3QubGFzdF9uYW1lIiwiJC5sYXN0X25hbWUiXSwiaWQiOiJiMmU1NTkxNi0yYjU1LTQ0MTEtYTQzMS0wOTlkOWMyMDQ0ZjAiLCJuYW1lIjoibGFzdF9uYW1lIiwiZmlsdGVyIjp7InR5cGUiOiJzdHJpbmciLCJwYXR0ZXJuIjoiR29uemFsZXoifX0seyJwYXRoIjpbIiQudmMuY3JlZGVudGlhbFN1YmplY3QuZW1haWxBZGRyZXNzIiwiJC5jcmVkZW50aWFsU3ViamVjdC5lbWFpbEFkZHJlc3MiLCIkLmVtYWlsQWRkcmVzcyJdLCJpZCI6IjU2ODRlMTY0LTA0MzAtNGYyYS1iMDI2LWRmNjcwYWZjNGVkNSIsIm5hbWUiOiJlbWFpbEFkZHJlc3MiLCJmaWx0ZXIiOnsidHlwZSI6InN0cmluZyIsInBhdHRlcm4iOiJ0ZXN0QGlvaGsuaW8ifX1dLCJsaW1pdF9kaXNjbG9zdXJlIjoicmVxdWlyZWQifX1dLCJmb3JtYXQiOnsic2RKd3QiOnsiYWxnIjpbIkVTMjU2SyJdfX19fQ=="},"format":"dif/presentation-exchange/definitions@v1.0"}],"thid":"11001ef6-f4c9-430c-84d7-ef74f0689e9f","ack":[],"direction":"SENT"}""" + ) + val requestData = msg.attachments.first().data.getDataAsJsonString() + + val presentationSubmission = + """{"presentation_submission":{"id":"780d0597-b2fd-4f22-a7dc-b29b4fa08cc1","definition_id":"5569abd7-2994-4068-be36-4025ebcaf20b","descriptor_map":[{"id":"0c4f5d84-0fb2-4e4c-b05b-6ce0f0afc586","format":"sdjwt","path":"${'$'}.verifiablePresentation[0]"}]},"verifiablePresentation":["eyJhbGciOiJFZERTQSJ9.eyJzdWIiOiJkaWQ6cHJpc206YXNkZmFzZGYiLCJfc2QiOlsiWWdQSV9kREQySVE4Z3d0MF9CbUs4Rk55MEpVVXoxbmJoTzQ2b0tuNUFsRSIsIi1nZ2wxR3g1akQzY2VoNHUxTHRXc0xQM3Z5MWtGNktlN2lCWXV5Ynd2eUUiLCJEQnM2Z0hhQTZZUldDSGVXZjlnRHA2eXVnaTVtX3dudkdPSno0aFdmdXlNIl0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiZGlkOnByaXNtOmNlMzQwM2I1YTczMzg4MzAzNWQ2ZWM0M2JhMDc1YTQxYzljYzBhMzI1Nzk3N2Q4MGM3NWQ2MzE5YWRlMGVkNzAiLCJleHAiOjE3MzU2ODk2NjEsImlhdCI6MTUxNjIzOTAyMn0.hVy9qpC9bVFpoGQ0G2s1GOkDgZxNGdhHIoVOaF8IMQU5pIvxZH1UQ1GzDpnhU0m_ZM8r1QpCeSVPuQKvMluDBQ~WyJtRzVscURxRFg2YWdraHpjX0NBR19RIiwiZmlyc3RfbmFtZSIsIkNyaXN0aWFuIl0~WyJSY2Z5X3ZCQm9wUjhLZFZqejRxbGJRIiwibGFzdF9uYW1lIiwiR29uemFsZXoiXQ~WyJpM0kzQWZJTERuSnVqRXkxS2ZNbFp3IiwiZW1haWxBZGRyZXNzIiwidGVzdEBpb2hrLmlvIl0~"]}""" + + assertTrue( + pollux.verifyPresentationSubmission( + presentationSubmission, + PresentationSubmissionOptionsSDJWT(presentationDefinitionRequest = requestData) + ) + ) } private suspend fun createVerificationTestCase(testCaseOptions: VerificationTestCase): Triple { @@ -1219,7 +1218,8 @@ class PolluxImplTest { }""" ) - val ecPrivateKey = pollux.parsePrivateKey(testCaseOptions.issuerPrv) + val ecPrivateKey = testCaseOptions.issuerPrv.jca() as ECPrivateKey +// pollux.parsePrivateKey(testCaseOptions.issuerPrv) val claims = JWTClaimsSet.Builder() .issuer(testCaseOptions.issuer.toString()) .audience(testCaseOptions.domain) @@ -1250,12 +1250,15 @@ class PolluxImplTest { doReturn(false) .`when`(pollux).isCredentialRevoked(any()) - val presentationSubmission = pollux.createJWTPresentationSubmission( - presentationDefinitionRequest = presentationDefinition, - credential = jwtCredential, - privateKey = testCaseOptions.holderPrv - ) + val presentationSubmission = jwtCredential.presentation( + attachmentFormat = CredentialType.PRESENTATION_EXCHANGE_DEFINITIONS.type, + request = presentationDefinition.encodeToByteArray(), + options = listOf( + CredentialOperationsOptions.SubjectDID(testCaseOptions.holder), + CredentialOperationsOptions.ExportableKey(testCaseOptions.holderPrv) + ) + ) return Triple(presentationDefinition, presentationSubmission, jwtString) } diff --git a/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/models/SDJWTCredentialTest.kt b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/models/SDJWTCredentialTest.kt new file mode 100644 index 000000000..c1abc53c3 --- /dev/null +++ b/edge-agent-sdk/src/commonTest/kotlin/org/hyperledger/identus/walletsdk/pollux/models/SDJWTCredentialTest.kt @@ -0,0 +1,104 @@ +package org.hyperledger.identus.walletsdk.pollux.models + +import com.nimbusds.jose.JWSAlgorithm +import com.nimbusds.jose.crypto.Ed25519Signer +import com.nimbusds.jose.jwk.KeyUse +import com.nimbusds.jose.jwk.OctetKeyPair +import com.nimbusds.jose.util.Base64URL +import com.nimbusds.jwt.SignedJWT +import eu.europa.ec.eudi.sdjwt.SdJwtIssuer +import eu.europa.ec.eudi.sdjwt.exp +import eu.europa.ec.eudi.sdjwt.iat +import eu.europa.ec.eudi.sdjwt.iss +import eu.europa.ec.eudi.sdjwt.nimbus +import eu.europa.ec.eudi.sdjwt.plain +import eu.europa.ec.eudi.sdjwt.sd +import eu.europa.ec.eudi.sdjwt.sdJwt +import eu.europa.ec.eudi.sdjwt.serialize +import eu.europa.ec.eudi.sdjwt.sub +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.put +import org.hyperledger.identus.apollo.base64.base64UrlDecoded +import org.hyperledger.identus.walletsdk.apollo.utils.Ed25519KeyPair +import org.hyperledger.identus.walletsdk.domain.models.CredentialOperationsOptions +import org.hyperledger.identus.walletsdk.domain.models.CredentialType +import org.hyperledger.identus.walletsdk.domain.models.DID +import org.hyperledger.identus.walletsdk.domain.models.Message +import org.junit.Test + +class SDJWTCredentialTest { + + var issuer: SdJwtIssuer? = null + + @Test + fun `test credential presentation`() = runTest { + val keyPair = Ed25519KeyPair.generateKeyPair() + + val credential = createSDJWTCredential(keyPair) + + val msg = Json.decodeFromString( + """{"id":"b3c7e130-9e90-454a-945c-58c7a4c9a181","piuri":"https://didcomm.atalaprism.io/present-proof/3.0/request-presentation","from":{"method":"peer","methodId":"asdf"},"to":{"method":"peer","methodId":"fdsafdsa"},"fromPrior":null,"body":"{\"proof_types\":[]}","created_time":"1727207690","expires_time_plus":"1727294090","attachments":[{"id":"6447a63c-17fa-4e22-a27a-ec0f6d5d753f","media_type":"application/json","data":{"base64":"eyJwcmVzZW50YXRpb25fZGVmaW5pdGlvbiI6eyJpZCI6IjU1NjlhYmQ3LTI5OTQtNDA2OC1iZTM2LTQwMjVlYmNhZjIwYiIsImlucHV0X2Rlc2NyaXB0b3JzIjpbeyJpZCI6IjU1ZmNhNzJlLWYyODQtNDNlZC1iMmNjLTNlODVmNzIwNmJjMSIsIm5hbWUiOiJQcmVzZW50YXRpb24iLCJwdXJwb3NlIjoiUHJlc2VudGF0aW9uIGRlZmluaXRpb24iLCJmb3JtYXQiOnsic2RKd3QiOnsiYWxnIjpbIkVTMjU2ayJdfX0sImNvbnN0cmFpbnRzIjp7ImZpZWxkcyI6W3sicGF0aCI6WyIkLnZjLmNyZWRlbnRpYWxTdWJqZWN0LmZpcnN0X25hbWUiLCIkLmNyZWRlbnRpYWxTdWJqZWN0LmZpcnN0X25hbWUiLCIkLmZpcnN0X25hbWUiXSwiaWQiOiIyNzVmNDcyYi0wNWE2LTRhNzAtODcxMS03NDVkODM1ZDlkZDUiLCJuYW1lIjoiZmlyc3RfbmFtZSIsImZpbHRlciI6eyJ0eXBlIjoic3RyaW5nIiwicGF0dGVybiI6IldvbmRlcmxhbmQifX0seyJwYXRoIjpbIiQudmMuY3JlZGVudGlhbFN1YmplY3QubGFzdF9uYW1lIiwiJC5jcmVkZW50aWFsU3ViamVjdC5sYXN0X25hbWUiLCIkLmxhc3RfbmFtZSJdLCJpZCI6IjA0YjhjMjMxLTE1YjQtNDI1NC1hNDRmLWZjMmM2NzAzMmE0MCIsIm5hbWUiOiJsYXN0X25hbWUiLCJmaWx0ZXIiOnsidHlwZSI6InN0cmluZyIsInBhdHRlcm4iOiJBbGljZSJ9fSx7InBhdGgiOlsiJC52Yy5jcmVkZW50aWFsU3ViamVjdC5lbWFpbEFkZHJlc3MiLCIkLmNyZWRlbnRpYWxTdWJqZWN0LmVtYWlsQWRkcmVzcyIsIiQuZW1haWxBZGRyZXNzIl0sImlkIjoiYmQxMWVlMjAtZDcyNS00ZjA2LWJmMWItNjE3NDFiMmZmM2FmIiwibmFtZSI6ImVtYWlsQWRkcmVzcyIsImZpbHRlciI6eyJ0eXBlIjoic3RyaW5nIiwicGF0dGVybiI6ImFsaWNlQHdvbmRlcmxhbmQuY29tIn19XSwibGltaXRfZGlzY2xvc3VyZSI6InJlcXVpcmVkIn19XSwiZm9ybWF0Ijp7InNkSnd0Ijp7ImFsZyI6WyJFUzI1NmsiXX19fX0"},"format":"dif/presentation-exchange/definitions@v1.0"}],"thid":"23f89714-7325-4048-a26b-978a0615efd7","ack":[],"direction":"SENT"}""" + ) + val requestData = msg.attachments.first().data.getDataAsJsonString() + + val presentation = credential.presentation( + attachmentFormat = CredentialType.SDJWT.type, + request = requestData.encodeToByteArray(), + listOf(CredentialOperationsOptions.DisclosingClaims(listOf("/emailAddress", "/first_name", "/last_name"))) + ) + val presentationSubmission = Json.decodeFromString(presentation) + assertTrue(presentationSubmission.verifiablePresentation.first().contains(".")) + val jws = presentationSubmission.verifiablePresentation.first() + val disclosures = jws.split("~") + var foundFirstName = false + var foundLastName = false + var foundEmail = false + for (i in 1..disclosures.size - 2) { + val value = disclosures[i].base64UrlDecoded + if (value.contains("first_name")) { + foundFirstName = true + } + if (value.contains("last_name")) { + foundLastName = true + } + if (value.contains("email")) { + foundEmail = true + } + } + assertTrue(foundFirstName && foundLastName && foundEmail) + } + + fun createSDJWTCredential(keyPair: Ed25519KeyPair): SDJWTCredential { + val subject = DID("did:prism:asdfasdf") + + val octet = OctetKeyPair.Builder(com.nimbusds.jose.jwk.Curve.Ed25519, Base64URL.encode(keyPair.publicKey.raw)) + .d(Base64URL.encode(keyPair.privateKey.raw)) + .keyUse(KeyUse.SIGNATURE) + .build() + + issuer = SdJwtIssuer + .nimbus( + signer = Ed25519Signer(octet), + signAlgorithm = JWSAlgorithm.EdDSA + ) + val sdjwt = issuer!!.issue( + sdJwt { + plain { + sub(subject.toString()) + iss("did:prism:ce3403b5a733883035d6ec43ba075a41c9cc0a3257977d80c75d6319ade0ed70") + iat(1516239022) + exp(1735689661) + } + sd { + put("first_name", "Cristian") + put("last_name", "Gonzalez") + put("emailAddress", "test@iohk.io") + } + } + ).getOrThrow().serialize() + + return SDJWTCredential.fromSDJwtString(sdjwt) + } +} diff --git a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/credentials/CredentialsAdapter.kt b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/credentials/CredentialsAdapter.kt index 9393f0ab8..48dbfce1d 100644 --- a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/credentials/CredentialsAdapter.kt +++ b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/credentials/CredentialsAdapter.kt @@ -10,6 +10,7 @@ import androidx.recyclerview.widget.RecyclerView import org.hyperledger.identus.walletsdk.domain.models.Credential import org.hyperledger.identus.walletsdk.pollux.models.AnonCredential import org.hyperledger.identus.walletsdk.pollux.models.JWTCredential +import org.hyperledger.identus.walletsdk.pollux.models.SDJWTCredential import org.hyperledger.identus.walletsdk.pollux.models.W3CCredential import org.hyperledger.identus.walletsdk.sampleapp.R import java.time.Instant @@ -100,6 +101,11 @@ class CredentialsAdapter( type.text = String.format(typeString, "Anoncred") expiryDate.text = String.format("Issuer: ${anon.credentialDefinitionID}") } + + SDJWTCredential::class -> { + val sdjwt = cred as SDJWTCredential + type.text = String.format(typeString, "SD-JWT") + } } } } diff --git a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/dids/DIDsFragment.kt b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/dids/DIDsFragment.kt index ab26c847b..10d160f36 100644 --- a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/dids/DIDsFragment.kt +++ b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/dids/DIDsFragment.kt @@ -32,6 +32,9 @@ class DIDsFragment : Fragment() { binding.createDid.setOnClickListener { viewModel.createPeerDID() } + binding.createPrismDid.setOnClickListener { + viewModel.createPrismDID() + } } override fun onDestroyView() { diff --git a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/dids/DIDsViewModel.kt b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/dids/DIDsViewModel.kt index 3fa6a671c..0ad6946cd 100644 --- a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/dids/DIDsViewModel.kt +++ b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/dids/DIDsViewModel.kt @@ -45,4 +45,11 @@ class DIDsViewModel(application: Application) : AndroidViewModel(application) { ) } } + + fun createPrismDID() { + viewModelScope.launch { + val sdk = Sdk.getInstance() + val did = sdk.agent.createNewPrismDID() + } + } } diff --git a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/messages/MessagesViewModel.kt b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/messages/MessagesViewModel.kt index 23aacf6be..c24b7a78e 100644 --- a/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/messages/MessagesViewModel.kt +++ b/sampleapp/src/main/java/org/hyperledger/identus/walletsdk/sampleapp/ui/messages/MessagesViewModel.kt @@ -12,18 +12,18 @@ import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.hyperledger.identus.walletsdk.castor.CastorImpl import org.hyperledger.identus.walletsdk.db.AppDatabase import org.hyperledger.identus.walletsdk.db.DatabaseClient import org.hyperledger.identus.walletsdk.domain.DIDCOMM_MESSAGING -import org.hyperledger.identus.walletsdk.domain.models.AnoncredsInputFieldFilter -import org.hyperledger.identus.walletsdk.domain.models.AnoncredsPresentationClaims import org.hyperledger.identus.walletsdk.domain.models.Credential import org.hyperledger.identus.walletsdk.domain.models.CredentialType import org.hyperledger.identus.walletsdk.domain.models.DID import org.hyperledger.identus.walletsdk.domain.models.DIDDocument +import org.hyperledger.identus.walletsdk.domain.models.InputFieldFilter import org.hyperledger.identus.walletsdk.domain.models.Message import org.hyperledger.identus.walletsdk.domain.models.ProvableCredential -import org.hyperledger.identus.walletsdk.domain.models.RequestedAttributes +import org.hyperledger.identus.walletsdk.domain.models.SDJWTPresentationClaims import org.hyperledger.identus.walletsdk.edgeagent.DIDCOMM1 import org.hyperledger.identus.walletsdk.edgeagent.EdgeAgentError import org.hyperledger.identus.walletsdk.edgeagent.protocols.ProtocolType @@ -114,28 +114,46 @@ class MessagesViewModel(application: Application) : AndroidViewModel(application // challenge = "challenge" // ) - // Anoncreds presentation request + // SD-JWT presentation request sdk.agent.initiatePresentationRequest( - type = CredentialType.ANONCREDS_PROOF_REQUEST, + type = CredentialType.SDJWT, toDID = DID(toDID), - presentationClaims = AnoncredsPresentationClaims( - predicates = mapOf( - "0_age" to AnoncredsInputFieldFilter( + presentationClaims = SDJWTPresentationClaims( + claims = mapOf( + "firstName" to InputFieldFilter( type = "string", - name = "age", - gte = 18 - ) - ), - attributes = mapOf( - "0_name" to RequestedAttributes( - "name", - setOf("name"), - emptyMap(), - null + pattern = "Wonderland" + ), + "emailAddress" to InputFieldFilter( + type = "string", + pattern = "alice@wonderland.com" ) ) ) ) + + // Anoncreds presentation request +// sdk.agent.initiatePresentationRequest( +// type = CredentialType.ANONCREDS_PROOF_REQUEST, +// toDID = DID(toDID), +// presentationClaims = AnoncredsPresentationClaims( +// predicates = mapOf( +// "0_age" to AnoncredsInputFieldFilter( +// type = "string", +// name = "age", +// gte = 18 +// ) +// ), +// attributes = mapOf( +// "0_name" to RequestedAttributes( +// "name", +// setOf("name"), +// emptyMap(), +// null +// ) +// ) +// ) +// ) } } @@ -193,11 +211,15 @@ class MessagesViewModel(application: Application) : AndroidViewModel(application viewModelScope.launch(handler) { messages.value?.find { it.id == uiMessage.id }?.let { message -> val sdk = Sdk.getInstance() - val valid = sdk.agent.handlePresentation(message) - if (valid) { - liveData.postValue("Valid!") - } else { - liveData.postValue("Not valid!") + try { + val valid = sdk.agent.handlePresentation(message) + if (valid) { + liveData.postValue("Valid!") + } else { + liveData.postValue("Not valid!") + } + } catch (e: Exception) { + liveData.postValue("Not valid. ${e.message}") } } } @@ -225,6 +247,9 @@ class MessagesViewModel(application: Application) : AndroidViewModel(application viewModelScope.launch { val offer = OfferCredential.fromMessage(message) val subjectDID = agent.createNewPrismDID() + + val x = (agent.castor as CastorImpl).resolvers.first().resolve(subjectDID.toString()) + val didDoc = agent.castor.resolveDID(subjectDID.toString()) val request = agent.prepareRequestCredentialWithIssuer( subjectDID, diff --git a/sampleapp/src/main/res/layout/fragment_dids.xml b/sampleapp/src/main/res/layout/fragment_dids.xml index f6b24d899..30968ab6f 100644 --- a/sampleapp/src/main/res/layout/fragment_dids.xml +++ b/sampleapp/src/main/res/layout/fragment_dids.xml @@ -25,4 +25,15 @@ app:tint="@color/white" android:contentDescription="@string/todo"/> + + \ No newline at end of file