diff --git a/castor/src/test/scala/org/hyperledger/identus/castor/core/service/MockDIDService.scala b/castor/src/test/scala/org/hyperledger/identus/castor/core/service/MockDIDService.scala index b911ff9a92..b4400673aa 100644 --- a/castor/src/test/scala/org/hyperledger/identus/castor/core/service/MockDIDService.scala +++ b/castor/src/test/scala/org/hyperledger/identus/castor/core/service/MockDIDService.scala @@ -38,45 +38,59 @@ object MockDIDService extends Mock[DIDService] { } } - def createDID( - verificationRelationship: VerificationRelationship + private def createDIDInternal( + verificationRelationship: VerificationRelationship, + addEd25519Key: Boolean = false ): (PrismDIDOperation.Create, Secp256k1KeyPair, DIDMetadata, DIDData) = { val masterKeyPair = Apollo.default.secp256k1.generateKeyPair val keyPair = Apollo.default.secp256k1.generateKeyPair - val createOperation = PrismDIDOperation.Create( - publicKeys = Seq( - InternalPublicKey( - id = KeyId("master-0"), - purpose = InternalKeyPurpose.Master, - publicKeyData = PublicKeyData.ECCompressedKeyData( - crv = EllipticCurve.SECP256K1, - data = Base64UrlString.fromByteArray(masterKeyPair.publicKey.getEncodedCompressed) - ) - ), - PublicKey( - id = KeyId("key-0"), - purpose = verificationRelationship, - publicKeyData = PublicKeyData.ECCompressedKeyData( - crv = EllipticCurve.SECP256K1, - data = Base64UrlString.fromByteArray(keyPair.publicKey.getEncodedCompressed) - ) - ), + val basePublicKeys = Seq( + InternalPublicKey( + id = KeyId("master-0"), + purpose = InternalKeyPurpose.Master, + publicKeyData = PublicKeyData.ECCompressedKeyData( + crv = EllipticCurve.SECP256K1, + data = Base64UrlString.fromByteArray(masterKeyPair.publicKey.getEncodedCompressed) + ) ), + PublicKey( + id = KeyId("key-0"), + purpose = verificationRelationship, + publicKeyData = PublicKeyData.ECCompressedKeyData( + crv = EllipticCurve.SECP256K1, + data = Base64UrlString.fromByteArray(keyPair.publicKey.getEncodedCompressed) + ) + ) + ) + + val publicKeys = if (addEd25519Key) { + val keyPair2 = Apollo.default.ed25519.generateKeyPair + basePublicKeys :+ PublicKey( + id = KeyId("key-1"), + purpose = verificationRelationship, + publicKeyData = PublicKeyData.ECKeyData( + crv = EllipticCurve.ED25519, + x = Base64UrlString.fromByteArray(keyPair2.publicKey.getEncoded), + y = Base64UrlString.fromByteArray(Array.emptyByteArray), + ) + ) + } else basePublicKeys + + val createOperation = PrismDIDOperation.Create( + publicKeys = publicKeys, services = Nil, context = Nil, ) val longFormDid = PrismDID.buildLongFormFromOperation(createOperation) - // val canonicalDid = longFormDid.asCanonical - val didMetadata = - DIDMetadata( - lastOperationHash = ArraySeq.from(longFormDid.stateHash.toByteArray), - canonicalId = None, // unpublished DID must not contain canonicalId - deactivated = false, // unpublished DID cannot be deactivated - created = None, // unpublished DID cannot have timestamp - updated = None // unpublished DID cannot have timestamp - ) + val didMetadata = DIDMetadata( + lastOperationHash = ArraySeq.from(longFormDid.stateHash.toByteArray), + canonicalId = None, + deactivated = false, + created = None, + updated = None + ) val didData = DIDData( id = longFormDid.asCanonical, publicKeys = createOperation.publicKeys.collect { case pk: PublicKey => pk }, @@ -87,6 +101,18 @@ object MockDIDService extends Mock[DIDService] { (createOperation, keyPair, didMetadata, didData) } + def createDIDOIDC( + verificationRelationship: VerificationRelationship + ): (PrismDIDOperation.Create, Secp256k1KeyPair, DIDMetadata, DIDData) = { + createDIDInternal(verificationRelationship, addEd25519Key = false) + } + + def createDID( + verificationRelationship: VerificationRelationship + ): (PrismDIDOperation.Create, Secp256k1KeyPair, DIDMetadata, DIDData) = { + createDIDInternal(verificationRelationship, addEd25519Key = true) + } + def resolveDIDExpectation(didMetadata: DIDMetadata, didData: DIDData): Expectation[DIDService] = MockDIDService.ResolveDID( assertion = Assertion.anything, diff --git a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala index fb4ce6f7ab..d1a5e65584 100644 --- a/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala +++ b/cloud-agent/service/server/src/test/scala/org/hyperledger/identus/oid4vci/domain/OIDCCredentialIssuerServiceSpec.scala @@ -64,10 +64,10 @@ object OIDCCredentialIssuerServiceSpec ) private val (_, issuerKp, issuerDidMetadata, issuerDidData) = - MockDIDService.createDID(VerificationRelationship.AssertionMethod) + MockDIDService.createDIDOIDC(VerificationRelationship.AssertionMethod) private val (holderOp, holderKp, holderDidMetadata, holderDidData) = - MockDIDService.createDID(VerificationRelationship.AssertionMethod) + MockDIDService.createDIDOIDC(VerificationRelationship.AssertionMethod) private val holderDidServiceExpectations = MockDIDService.resolveDIDExpectation(holderDidMetadata, holderDidData) diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialServiceError.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialServiceError.scala index a0ffdbe944..6ad634ce42 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialServiceError.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/error/CredentialServiceError.scala @@ -120,7 +120,13 @@ object CredentialServiceError { StatusCode.NotFound, s"A key with the given purpose was not found in the DID: did=${did.toString}, purpose=${verificationRelationship.name}" ) - + final case class MultipleKeysWithSamePurposeFoundInDID( + did: PrismDID, + verificationRelationship: VerificationRelationship + ) extends CredentialServiceError( + StatusCode.BadRequest, + s"A key with the given purpose was found multiple times in the DID: did=${did.toString}, purpose=${verificationRelationship.name}" + ) final case class InvalidCredentialRequest(cause: String) extends CredentialServiceError( StatusCode.BadRequest, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala index a95e80925e..bdd741e155 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImpl.scala @@ -223,7 +223,7 @@ class CredentialServiceImpl( claims = attributes, thid = thid, UUID.randomUUID().toString, - "domain", + "domain", // TODO remove the hardcoded domain IssueCredentialOfferFormat.JWT ) record <- createIssueCredentialRecord( @@ -559,8 +559,8 @@ class CredentialServiceImpl( private[this] def getKeyId( did: PrismDID, verificationRelationship: VerificationRelationship, - ellipticCurve: EllipticCurve - ): UIO[KeyId] = { + keyId: Option[KeyId] + ): UIO[PublicKey] = { for { maybeDidData <- didService .resolveDID(did) @@ -569,15 +569,25 @@ class CredentialServiceImpl( .fromOption(maybeDidData) .mapError(_ => DIDNotResolved(did)) .orDieAsUnmanagedFailure - keyId <- ZIO - .fromOption( - didData._2.publicKeys - .find(pk => pk.purpose == verificationRelationship && pk.publicKeyData.crv == ellipticCurve) - .map(_.id) - ) - .mapError(_ => KeyNotFoundInDID(did, verificationRelationship)) - .orDieAsUnmanagedFailure - } yield keyId + matchingKeys = didData._2.publicKeys.filter(pk => pk.purpose == verificationRelationship) + result <- (matchingKeys, keyId) match { + case (Seq(), _) => + ZIO.fail(KeyNotFoundInDID(did, verificationRelationship)).orDieAsUnmanagedFailure + case (Seq(singleKey), None) => + ZIO.succeed(singleKey) + case (multipleKeys, Some(kid)) => + ZIO + .fromOption(multipleKeys.find(_.id.value.endsWith(kid.value))) + .mapError(_ => KeyNotFoundInDID(did, verificationRelationship)) + .orDieAsUnmanagedFailure + case (multipleKeys, None) => + ZIO + .fail( + MultipleKeysWithSamePurposeFoundInDID(did, verificationRelationship) + ) + .orDieAsUnmanagedFailure + } + } yield result } override def getJwtIssuer( @@ -586,34 +596,46 @@ class CredentialServiceImpl( keyId: Option[KeyId] = None ): URIO[WalletAccessContext, JwtIssuer] = { for { - issuingKeyId <- getKeyId(jwtIssuerDID, verificationRelationship, EllipticCurve.SECP256K1) - ecKeyPair <- managedDIDService - .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId) + issuingPublicKey <- getKeyId(jwtIssuerDID, verificationRelationship, keyId) + jwtIssuer <- managedDIDService + .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingPublicKey.id) .flatMap { - case Some(keyPair: Secp256k1KeyPair) => ZIO.some(keyPair) - case _ => ZIO.none + case Some(keyPair: Secp256k1KeyPair) => { + val jwtIssuer = JwtIssuer( + jwtIssuerDID.did, + ES256KSigner(keyPair.privateKey.toJavaPrivateKey, keyId), + keyPair.publicKey.toJavaPublicKey + ) + ZIO.some(jwtIssuer) + } + case Some(keyPair: Ed25519KeyPair) => { + val jwtIssuer = JwtIssuer( + jwtIssuerDID.did, + EdSigner(keyPair, keyId), + keyPair.publicKey.toJava + ) + ZIO.some(jwtIssuer) + } + case _ => ZIO.none } - .someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingKeyId, "Secp256k1")) + .someOrFail( + KeyPairNotFoundInWallet(jwtIssuerDID, issuingPublicKey.id, issuingPublicKey.publicKeyData.crv.name) + ) .orDieAsUnmanagedFailure - Secp256k1KeyPair(publicKey, privateKey) = ecKeyPair - jwtIssuer = JwtIssuer( - jwtIssuerDID.did, - ES256KSigner(privateKey.toJavaPrivateKey, keyId), - publicKey.toJavaPublicKey - ) } yield jwtIssuer } private def getEd25519SigningKeyPair( jwtIssuerDID: PrismDID, - verificationRelationship: VerificationRelationship + verificationRelationship: VerificationRelationship, + keyId: Option[KeyId] = None ): URIO[WalletAccessContext, Ed25519KeyPair] = { for { - issuingKeyId <- getKeyId(jwtIssuerDID, verificationRelationship, EllipticCurve.ED25519) + issuingPublicKey <- getKeyId(jwtIssuerDID, verificationRelationship, keyId) ed25519keyPair <- managedDIDService - .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId) + .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingPublicKey.id) .map(_.collect { case keyPair: Ed25519KeyPair => keyPair }) - .someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingKeyId, "Ed25519")) + .someOrFail(KeyPairNotFoundInWallet(jwtIssuerDID, issuingPublicKey.id, issuingPublicKey.publicKeyData.crv.name)) .orDieAsUnmanagedFailure } yield ed25519keyPair } @@ -635,7 +657,7 @@ class CredentialServiceImpl( keyId: Option[KeyId] ): URIO[WalletAccessContext, JwtIssuer] = { for { - ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship) + ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship, keyId) } yield { JwtIssuer( jwtIssuerDID.did, @@ -1163,7 +1185,7 @@ class CredentialServiceImpl( .orElse(ZIO.dieMessage(s"Offer credential data not found in record: ${recordId.value}")) preview = offerCredentialData.body.credential_preview claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes).orDieAsUnmanagedFailure - jwtIssuer <- getJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod) + jwtIssuer <- getJwtIssuer(longFormPrismDID, VerificationRelationship.AssertionMethod, record.keyId) jwtPresentation <- validateRequestCredentialDataProof(maybeOfferOptions, requestJwt) .tapError(error => credentialRepository @@ -1243,7 +1265,11 @@ class CredentialServiceImpl( case ZValidation.Success(log, header) => ZIO.succeed(header) case ZValidation.Failure(log, failure) => ZIO.fail(VCJwtHeaderParsingError(s"Extraction of JwtHeader failed ${failure.toChunk.toString}")) - ed25519KeyPair <- getEd25519SigningKeyPair(longFormPrismDID, VerificationRelationship.AssertionMethod) + ed25519KeyPair <- getEd25519SigningKeyPair( + longFormPrismDID, + VerificationRelationship.AssertionMethod, + record.keyId + ) sdJwtPrivateKey = sdjwt.IssuerPrivateKey(ed25519KeyPair.privateKey) jsonWebKey <- didResolver.resolve(jwtPresentation.iss) flatMap { case failed: DIDResolutionFailed => diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala index d59c0cd30d..60664cdb29 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/CredentialRepositoryInMemory.scala @@ -269,6 +269,7 @@ class CredentialRepositoryInMemory( updatedAt = Some(Instant.now), protocolState = protocolState, subjectId = Some(subjectId), + keyId = keyId, metaRetries = maxRetries, metaLastFailure = None, ) diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala index 0e15ad2d55..ab9f7b051b 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceImplSpec.scala @@ -2,6 +2,7 @@ package org.hyperledger.identus.pollux.core.service import io.circe.syntax.* import io.circe.Json +import org.bouncycastle.jce.provider.BouncyCastleProvider import org.hyperledger.identus.agent.walletapi.service.MockManagedDIDService import org.hyperledger.identus.castor.core.model.did.* import org.hyperledger.identus.castor.core.model.did.VerificationRelationship.AssertionMethod @@ -15,6 +16,7 @@ import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError.* import org.hyperledger.identus.pollux.core.model.schema.CredentialDefinition import org.hyperledger.identus.pollux.core.model.IssueCredentialRecord.{ProtocolState, Role} import org.hyperledger.identus.pollux.core.service.uriResolvers.ResourceUrlResolver +import org.hyperledger.identus.pollux.core.service.CredentialServiceImplSpec.test import org.hyperledger.identus.pollux.vc.jwt.{CredentialIssuer, JWT, JwtCredential, JwtCredentialPayload} import org.hyperledger.identus.shared.models.{KeyId, UnmanagedFailureException, WalletAccessContext, WalletId} import zio.* @@ -23,10 +25,11 @@ import zio.test.* import zio.test.Assertion.* import java.nio.charset.StandardCharsets +import java.security.Security import java.util.{Base64, UUID} object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceSpecHelper { - + Security.addProvider(new BouncyCastleProvider()); override def spec = suite("CredentialServiceImpl")( singleWalletJWTCredentialSpec, singleWalletAnonCredsCredentialSpec, @@ -446,7 +449,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS offer = offerCredential() subjectId = "did:prism:60821d6833158c93fde5bb6a40d69996a683bf1fa5cdf32c458395b2887597c3" offerReceivedRecord <- holderSvc.receiveCredentialOffer(offer) - _ <- holderSvc.acceptCredentialOffer(offerReceivedRecord.id, Some(subjectId), Some(KeyId("my-key-id"))) + _ <- holderSvc.acceptCredentialOffer(offerReceivedRecord.id, Some(subjectId), Some(KeyId("key-0"))) _ <- holderSvc.generateJWTCredentialRequest(offerReceivedRecord.id) _ <- holderSvc.markRequestSent(offerReceivedRecord.id) issue = issueCredential(thid = Some(offerReceivedRecord.thid)) @@ -462,7 +465,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS offer = offerCredential() subjectId = "did:prism:60821d6833158c93fde5bb6a40d69996a683bf1fa5cdf32c458395b2887597c3" offerReceivedRecord <- holderSvc.receiveCredentialOffer(offer) - _ <- holderSvc.acceptCredentialOffer(offerReceivedRecord.id, Some(subjectId), Some(KeyId("my-key-id"))) + _ <- holderSvc.acceptCredentialOffer(offerReceivedRecord.id, Some(subjectId), Some(KeyId("key-0"))) _ <- holderSvc.generateJWTCredentialRequest(offerReceivedRecord.id) _ <- holderSvc.markRequestSent(offerReceivedRecord.id) issue = issueCredential(thid = Some(offerReceivedRecord.thid)) @@ -481,7 +484,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS offer = offerCredential() subjectId = "did:prism:60821d6833158c93fde5bb6a40d69996a683bf1fa5cdf32c458395b2887597c3" offerReceivedRecord <- holderSvc.receiveCredentialOffer(offer) - _ <- holderSvc.acceptCredentialOffer(offerReceivedRecord.id, Some(subjectId), Some(KeyId("my-key-id"))) + _ <- holderSvc.acceptCredentialOffer(offerReceivedRecord.id, Some(subjectId), Some(KeyId("key-0"))) _ <- holderSvc.generateJWTCredentialRequest(offerReceivedRecord.id) _ <- holderSvc.markRequestSent(offerReceivedRecord.id) issue = issueCredential(thid = Some(DidCommID())) @@ -498,7 +501,62 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS issuerSvc <- ZIO.service[CredentialService].provideSomeLayer(credentialServiceLayer) holderSvc <- ZIO.service[CredentialService].provideSomeLayer(credentialServiceLayer) // Issuer creates offer - offerCreatedRecord <- issuerSvc.createJWTIssueCredentialRecord() + offerCreatedRecord <- issuerSvc.createJWTIssueCredentialRecord(kidIssuer = Some(KeyId("key-0"))) + issuerRecordId = offerCreatedRecord.id + // Issuer sends offer + _ <- issuerSvc.markOfferSent(issuerRecordId) + msg <- ZIO.fromEither(offerCreatedRecord.offerCredentialData.get.makeMessage.asJson.as[Message]) + // Holder receives offer + offerCredential <- ZIO.fromEither(OfferCredential.readFromMessage(msg)) + offerReceivedRecord <- holderSvc.receiveCredentialOffer(offerCredential) + holderRecordId = offerReceivedRecord.id + subjectId = "did:prism:60821d6833158c93fde5bb6a40d69996a683bf1fa5cdf32c458395b2887597c3" + // Holder accepts offer + _ <- holderSvc.acceptCredentialOffer(holderRecordId, Some(subjectId), Some(KeyId("key-0"))) + // Holder generates proof + requestGeneratedRecord <- holderSvc.generateJWTCredentialRequest(offerReceivedRecord.id) + // Holder sends offer + _ <- holderSvc.markRequestSent(holderRecordId) + msg <- ZIO.fromEither(requestGeneratedRecord.requestCredentialData.get.makeMessage.asJson.as[Message]) + // Issuer receives request + requestCredential <- ZIO.fromEither(RequestCredential.readFromMessage(msg)) + requestReceivedRecord <- issuerSvc.receiveCredentialRequest(requestCredential) + // Issuer accepts request + requestAcceptedRecord <- issuerSvc.acceptCredentialRequest(issuerRecordId) + // Issuer generates credential + credentialGenerateRecord <- issuerSvc.generateJWTCredential( + issuerRecordId, + "status-list-registry" + ) + decodedJWT <- credentialGenerateRecord.issueCredentialData.get.attachments.head.data match { + case MyBase64(value) => + val ba = new String(Base64.getUrlDecoder.decode(value)) + JwtCredential.decodeJwt(JWT(ba)) + case _ => ZIO.fail("Error") + } + // Issuer sends credential + _ <- issuerSvc.markCredentialSent(issuerRecordId) + msg <- ZIO.fromEither(credentialGenerateRecord.issueCredentialData.get.makeMessage.asJson.as[Message]) + // Holder receives credential + issueCredential <- ZIO.fromEither(IssueCredential.readFromMessage(msg)) + _ <- holderSvc.receiveCredentialIssue(issueCredential) + } yield assertTrue( + decodedJWT.issuer == + CredentialIssuer( + id = decodedJWT.iss, + `type` = "Profile" + ) + ) + }.provideSomeLayer( + (holderDidServiceExpectations ++ issuerDidServiceExpectations).toLayer + ++ (holderManagedDIDServiceExpectations ++ issuerManagedDIDServiceExpectations).toLayer + ), + test("Happy flow is successfully executed with ED25519") { + for { + issuerSvc <- ZIO.service[CredentialService].provideSomeLayer(credentialServiceLayer) + holderSvc <- ZIO.service[CredentialService].provideSomeLayer(credentialServiceLayer) + // Issuer creates offer + offerCreatedRecord <- issuerSvc.createJWTIssueCredentialRecord(kidIssuer = Some(KeyId("key-1"))) issuerRecordId = offerCreatedRecord.id // Issuer sends offer _ <- issuerSvc.markOfferSent(issuerRecordId) @@ -509,7 +567,7 @@ object CredentialServiceImplSpec extends MockSpecDefault with CredentialServiceS holderRecordId = offerReceivedRecord.id subjectId = "did:prism:60821d6833158c93fde5bb6a40d69996a683bf1fa5cdf32c458395b2887597c3" // Holder accepts offer - _ <- holderSvc.acceptCredentialOffer(holderRecordId, Some(subjectId), Some(KeyId("my-key-id"))) + _ <- holderSvc.acceptCredentialOffer(holderRecordId, Some(subjectId), Some(KeyId("key-1"))) // Holder generates proof requestGeneratedRecord <- holderSvc.generateJWTCredentialRequest(offerReceivedRecord.id) // Holder sends offer diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala index 8ae2fd6602..c8c608959f 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala @@ -18,7 +18,7 @@ import org.hyperledger.identus.pollux.prex.{ClaimFormat, Ldp, PresentationDefini import org.hyperledger.identus.pollux.vc.jwt.* import org.hyperledger.identus.shared.http.UriResolver import org.hyperledger.identus.shared.messaging.{MessagingService, MessagingServiceConfig, WalletIdAndRecordId} -import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} +import org.hyperledger.identus.shared.models.{KeyId, WalletAccessContext, WalletId} import zio.* import java.util.UUID @@ -123,7 +123,8 @@ trait CredentialServiceSpecHelper { |""".stripMargin) .getOrElse(Json.Null), validityPeriod: Option[Double] = None, - automaticIssuance: Option[Boolean] = None + automaticIssuance: Option[Boolean] = None, + kidIssuer: Option[KeyId] = None ) = for { issuingDID <- ZIO.fromEither( PrismDID.buildCanonicalFromSuffix("5c2576867a5544e5ad05cdc94f02c664b99ff65c28e8b62aada767244c2199fe") @@ -131,7 +132,7 @@ trait CredentialServiceSpecHelper { record <- svc.createJWTIssueCredentialRecord( pairwiseIssuerDID = pairwiseIssuerDID, pairwiseHolderDID = pairwiseHolderDID, - kidIssuer = None, + kidIssuer = kidIssuer, thid = thid, maybeSchemaIds = maybeSchemaIds, claims = claims, diff --git a/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt b/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt index 7dcc6ec6e3..c4c30ca83c 100644 --- a/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt +++ b/tests/integration-tests/src/test/kotlin/common/DidPurpose.kt @@ -15,6 +15,15 @@ enum class DidPurpose { override val services = mutableListOf() }, JWT { + override val publicKeys = mutableListOf( + ManagedDIDKeyTemplate("auth-1", Purpose.AUTHENTICATION, Curve.SECP256K1), + ManagedDIDKeyTemplate("auth-2", Purpose.AUTHENTICATION, Curve.ED25519), + ManagedDIDKeyTemplate("assertion-1", Purpose.ASSERTION_METHOD, Curve.SECP256K1), + ManagedDIDKeyTemplate("assertion-2", Purpose.ASSERTION_METHOD, Curve.ED25519), + ) + override val services = mutableListOf() + }, + OIDC_JWT { override val publicKeys = mutableListOf( ManagedDIDKeyTemplate("auth-1", Purpose.AUTHENTICATION, Curve.SECP256K1), ManagedDIDKeyTemplate("auth-2", Purpose.AUTHENTICATION, Curve.ED25519), diff --git a/tests/integration-tests/src/test/kotlin/steps/connectionless/ConnectionLessSteps.kt b/tests/integration-tests/src/test/kotlin/steps/connectionless/ConnectionLessSteps.kt index 24f2e281ac..ec7a49a0df 100644 --- a/tests/integration-tests/src/test/kotlin/steps/connectionless/ConnectionLessSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/connectionless/ConnectionLessSteps.kt @@ -29,6 +29,7 @@ class ConnectionLessSteps { val credentialOfferRequest = CreateIssueCredentialRecordRequest( claims = claims, issuingDID = did, + issuingKid = "assertion-1", validityPeriod = 3600.0, credentialFormat = credentialFormat, automaticIssuance = false, diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt index a25abc6ade..5ca7c1d96b 100644 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/JwtCredentialSteps.kt @@ -20,6 +20,7 @@ class JwtCredentialSteps { didForm: String, schemaGuid: String?, claims: Map, + issuingKid: String?, ) { val did: String = if (didForm == "short") { issuer.recall("shortFormDid") @@ -38,6 +39,7 @@ class JwtCredentialSteps { schemaId = schemaId?.let { listOf(it) }, claims = claims, issuingDID = did, + issuingKid = issuingKid, connectionId = issuer.recall("connection-with-${holder.name}").connectionId, validityPeriod = 3600.0, credentialFormat = "JWT", @@ -66,7 +68,17 @@ class JwtCredentialSteps { "firstName" to "FirstName", "lastName" to "LastName", ) - sendCredentialOffer(issuer, holder, format, null, claims) + sendCredentialOffer(issuer, holder, format, null, claims, "assertion-1") + saveCredentialOffer(issuer, holder) + } + + @When("{actor} offers a jwt credential to {actor} with {string} form DID using issuingKid {string}") + fun issuerOffersAJwtCredentialWithIssuingKeyId(issuer: Actor, holder: Actor, format: String, issuingKid: String?) { + val claims = linkedMapOf( + "firstName" to "FirstName", + "lastName" to "LastName", + ) + sendCredentialOffer(issuer, holder, format, null, claims, issuingKid) saveCredentialOffer(issuer, holder) } @@ -79,7 +91,7 @@ class JwtCredentialSteps { ) { val schemaGuid = issuer.recall(schema.name) val claims = schema.claims - sendCredentialOffer(issuer, holder, format, schemaGuid, claims) + sendCredentialOffer(issuer, holder, format, schemaGuid, claims, "assertion-1") saveCredentialOffer(issuer, holder) } @@ -95,7 +107,7 @@ class JwtCredentialSteps { "name" to "Name", "surname" to "Surname", ) - sendCredentialOffer(issuer, holder, format, schemaGuid, claims) + sendCredentialOffer(issuer, holder, format, schemaGuid, claims, "assertion-1") } @When("{actor} accepts jwt credential offer") @@ -103,7 +115,17 @@ class JwtCredentialSteps { val recordId = holder.recall("recordId") holder.attemptsTo( Post.to("/issue-credentials/records/$recordId/accept-offer") - .body(AcceptCredentialOfferRequest(holder.recall("longFormDid"))), + .body(AcceptCredentialOfferRequest(holder.recall("longFormDid"), holder.recall("kidSecp256K1"))), + Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), + ) + } + + @When("{actor} accepts jwt credential offer with keyId {string}") + fun holderAcceptsJwtCredentialOfferForJwtWithKeyId(holder: Actor, keyId: String?) { + val recordId = holder.recall("recordId") + holder.attemptsTo( + Post.to("/issue-credentials/records/$recordId/accept-offer") + .body(AcceptCredentialOfferRequest(holder.recall("longFormDid"), keyId)), Ensure.thatTheLastResponse().statusCode().isEqualTo(SC_OK), ) } diff --git a/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt b/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt index d7c3addafb..8bff650f21 100644 --- a/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/did/PublishDidSteps.kt @@ -70,6 +70,8 @@ class PublishDidSteps { val did = SerenityRest.lastResponse().get() actor.remember("longFormDid", managedDid.longFormDid) + actor.remember("kidSecp256K1", "auth-1") + actor.remember("kidEd25519", "auth-2") actor.remember("shortFormDid", did.did) actor.remember("didPurpose", didPurpose) actor.forget("hasPublishedDid") diff --git a/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature b/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature index 79b541c1f6..6258cf5fdb 100644 --- a/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature +++ b/tests/integration-tests/src/test/resources/features/credential/jwt/issuance.feature @@ -12,6 +12,16 @@ Feature: Issue JWT credential And Issuer issues the credential Then Holder receives the issued credential + Scenario: Issuing jwt credential with published PRISM DID using Ed25519 + Given Issuer and Holder have an existing connection + And Issuer has a published DID for JWT + And Holder has an unpublished DID for JWT + When Issuer offers a jwt credential to Holder with "short" form DID using issuingKid "assertion-2" + And Holder receives the credential offer + And Holder accepts jwt credential offer with keyId "auth-2" + And Issuer issues the credential + Then Holder receives the issued credential + Scenario: Issuing jwt credential with a schema Given Issuer and Holder have an existing connection And Issuer has a published DID for JWT diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature b/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature index 53d010c2c5..00c7bf2f45 100644 --- a/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature +++ b/tests/integration-tests/src/test/resources/features/oid4vci/issue_jwt.feature @@ -2,7 +2,7 @@ Feature: Issue JWT Credentials using OID4VCI authorization code flow Background: - Given Issuer has a published DID for JWT + Given Issuer has a published DID for OIDC_JWT And Issuer has published STUDENT_SCHEMA schema And Issuer has an existing oid4vci issuer And Issuer has "StudentProfile" credential configuration created from STUDENT_SCHEMA diff --git a/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature b/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature index cacdeb6cdb..669fe6977e 100644 --- a/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature +++ b/tests/integration-tests/src/test/resources/features/oid4vci/manage_credential_config.feature @@ -2,7 +2,7 @@ Feature: Manage OID4VCI credential configuration Background: - Given Issuer has a published DID for JWT + Given Issuer has a published DID for OIDC_JWT And Issuer has published STUDENT_SCHEMA schema And Issuer has an existing oid4vci issuer