From a4ce87fd709102e0a5e597e5ba50891e01d46a51 Mon Sep 17 00:00:00 2001 From: Fabio Pinheiro Date: Tue, 18 Jun 2024 15:47:33 +0100 Subject: [PATCH 1/3] feat: implement ADR Use ZIO Failures and Defects Effectively - Mercury should not throw exceptions (#1192) Signed-off-by: FabioPinheiro Co-authored-by: Yurii Shynbuiev - IOHK --- .../server/jobs/IssueBackgroundJobs.scala | 9 ++--- .../controller/DIDCommControllerImpl.scala | 1 - .../identus/mercury/DidCommX.scala | 6 +-- .../identus/mercury/MessagingService.scala | 19 ++++++--- .../identus/mercury/model/Conversions.scala | 2 +- .../identus/mercury/model/package.scala | 3 +- .../mercury/CoordinateMediationPrograms.scala | 9 +++-- .../hyperledger/identus/mercury/DidOps.scala | 2 +- .../protocol/issuecredential/Credential.scala | 30 -------------- .../issuecredential/OfferCredential.scala | 40 +++++++++++-------- .../issuecredential/ProposeCredential.scala | 34 +++++++++------- .../protocol/issuecredential/Utils.scala | 2 +- .../UtilsCredentialSpec.scala | 17 ++++---- .../JdbcPresentationRepository.scala | 7 ++-- 14 files changed, 84 insertions(+), 97 deletions(-) delete mode 100644 mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/Credential.scala diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala index 1f67b6d93c..0780309a28 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/agent/server/jobs/IssueBackgroundJobs.scala @@ -615,11 +615,9 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { aux .tapError( { - case walletNotFound: WalletNotFoundError => - ZIO.logErrorCause( - s"Issue Credential- Error processing record: ${record.id}", - Cause.fail(walletNotFound) - ) + case walletNotFound: WalletNotFoundError => ZIO.unit + case CredentialServiceError.RecordNotFound(_, _) => ZIO.unit + case CredentialServiceError.UnsupportedDidFormat(_) => ZIO.unit case ((walletAccessContext, e)) => for { credentialService <- ZIO.service[CredentialService] @@ -627,7 +625,6 @@ object IssueBackgroundJobs extends BackgroundJobsHelper { .reportProcessingFailure(record.id, Some(e.toString)) .provideSomeLayer(ZLayer.succeed(walletAccessContext)) } yield () - } ) .catchAll(e => ZIO.logErrorCause(s"Issue Credential - Error processing record: ${record.id} ", Cause.fail(e))) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/didcomm/controller/DIDCommControllerImpl.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/didcomm/controller/DIDCommControllerImpl.scala index feef2638ce..44d7e1e8e5 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/didcomm/controller/DIDCommControllerImpl.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/didcomm/controller/DIDCommControllerImpl.scala @@ -55,7 +55,6 @@ class DIDCommControllerImpl( .catchAll { case f: Failure => ZIO.fail(f) case _: DIDCommMessageParsingError => ZIO.fail(UnexpectedError(StatusCode.BadRequest)) - case _: PresentationError => ZIO.fail(UnexpectedError(StatusCode.UnprocessableContent)) } .provideSomeLayer(ZLayer.succeed(msgAndContext._2)) } yield () diff --git a/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/DidCommX.scala b/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/DidCommX.scala index 7e8b362eb3..8e7e4f4d11 100644 --- a/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/DidCommX.scala +++ b/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/DidCommX.scala @@ -20,10 +20,6 @@ class DidCommX() extends DidOps /* with DidAgent with DIDResolver */ { new DIDComm(UniversalDidResolver, AgentPeerService.getSecretResolverInMemory(agent)) } - // override def id: DidId = fixme // FIXME the Secret is on org.didcommx.didcomm.model.DIDComm ... - - // override def resolveDID(did: DidId): Task[DIDDoc] = UniversalDidResolver.resolveDID(did) - override def packSigned(msg: Message): URIO[DidAgent, SignedMesage] = for { agent <- ZIO.service[DidAgent] params = new PackSignedParams.Builder(msg, agent.id.value).build() @@ -39,7 +35,7 @@ class DidCommX() extends DidOps /* with DidAgent with DIDResolver */ { ret = didCommFor(agent).packEncrypted(params) } yield (ret) - // FIXME theoretically DidAgent is not needed + // TODO theoretically DidAgent is not needed override def packEncryptedAnon(msg: Message, to: DidId): URIO[DidAgent, EncryptedMessage] = for { agent <- ZIO.service[DidAgent] params = new PackEncryptedParams.Builder(msg, to.value) diff --git a/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/MessagingService.scala b/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/MessagingService.scala index 1e28fbce2d..06de456b74 100644 --- a/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/MessagingService.scala +++ b/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/MessagingService.scala @@ -98,7 +98,12 @@ object MessagingService { finalMessage <- makeMessage(forwardMessage.asMessage) // Maybe it needs a double warping } yield finalMessage case ServiceEndpoint(uri, _, Some(routingKeys)) => - ZIO.log(s"RoutingDID: $routingKeys") *> ??? // ZIO.fail(???) // FIXME no support for routingKeys + ZIO.log(s"RoutingDID: $routingKeys") *> + ZIO.fail( + SendMessageError( + RuntimeException("routingKeys is not supported at the moment") + ) + ) case s @ ServiceEndpoint(_, _, None) => ZIO.logError(s"Unxpected ServiceEndpoint $s") *> ZIO.fail( SendMessageError(new RuntimeException(s"Unxpected ServiceEndpoint $s")) @@ -116,11 +121,15 @@ object MessagingService { auxFinalMessage <- makeMessage(msg) MessageAndAddress(finalMessage, serviceEndpoint) = auxFinalMessage didCommService <- ZIO.service[DidOps] + to <- finalMessage.to match { + case Seq() => ZIO.fail(SendMessageError(new RuntimeException("Message must have a recipient"))) + case firstTo +: Seq() => ZIO.succeed(firstTo) + case all @ (firstTo +: _) => + ZIO.logWarning(s"Message have multi recipients: $all") *> ZIO.succeed(firstTo) + } encryptedMessage <- - if (finalMessage.`type` == ForwardMessage.PIURI) - didCommService.packEncryptedAnon(msg = finalMessage, to = finalMessage.to.head) // TODO Head - else - didCommService.packEncrypted(msg = finalMessage, to = finalMessage.to.head) // TODO Head + if (finalMessage.`type` == ForwardMessage.PIURI) didCommService.packEncryptedAnon(msg = finalMessage, to = to) + else didCommService.packEncrypted(msg = finalMessage, to = to) _ <- ZIO.log(s"Sending a Message to '$serviceEndpoint'") resp <- org.hyperledger.identus.mercury.HttpClient diff --git a/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/model/Conversions.scala b/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/model/Conversions.scala index a5314d4fbc..c5b9d3d082 100644 --- a/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/model/Conversions.scala +++ b/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/model/Conversions.scala @@ -37,7 +37,7 @@ given Conversion[Message, org.didcommx.didcomm.message.Message] with { msg.to.foreach(did => aux.to(Seq(did.value).asJava)) msg.pleaseAck.foreach { seq => // https://identity.foundation/didcomm-messaging/spec/#acks - aux.pleaseAck(true) // FIXME lib limitation the field pleaseAck MUST be a Array of string + aux.pleaseAck(true) // NOTE lib limitation the field pleaseAck MUST be a Array of string } msg.ack.flatMap(_.headOption).foreach(str => aux.ack(str)) // NOTE: headOption becuase DidCommx only support one ack msg.thid.foreach(str => aux.thid(str)) diff --git a/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/model/package.scala b/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/model/package.scala index 4a185ab60a..3437e3fde0 100644 --- a/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/model/package.scala +++ b/mercury/agent-didcommx/src/main/scala/org/hyperledger/identus/mercury/model/package.scala @@ -43,7 +43,8 @@ object UnpackMessageImp { pleaseAck = Option(msg.getPleaseAck()) .flatMap { // https://identity.foundation/didcomm-messaging/spec/#acks - case java.lang.Boolean.TRUE => Some(Seq.empty) // FIXME getPleaseAck MUST return a Array + case java.lang.Boolean.TRUE => + Some(Seq.empty) // NOTE lib limitation the field pleaseAck MUST be a Array of string case java.lang.Boolean.FALSE => None } ) diff --git a/mercury/agent/src/main/scala/org/hyperledger/identus/mercury/CoordinateMediationPrograms.scala b/mercury/agent/src/main/scala/org/hyperledger/identus/mercury/CoordinateMediationPrograms.scala index 1210b1fe11..2bd1aaea45 100644 --- a/mercury/agent/src/main/scala/org/hyperledger/identus/mercury/CoordinateMediationPrograms.scala +++ b/mercury/agent/src/main/scala/org/hyperledger/identus/mercury/CoordinateMediationPrograms.scala @@ -32,12 +32,15 @@ object CoordinateMediationPrograms { _ <- ZIO.log("#### Send Mediation request ####") link <- InvitationPrograms .getInvitationProgram(mediatorURL + "/oob_url") - .map(_.toOption) // FIXME + .flatMap { + case Left(value) => ZIO.fail(value) + case Right(value) => ZIO.succeed(value) + } opsService <- ZIO.service[DidOps] agentService <- ZIO.service[DidAgent] - planMessage = link.map(to => replyToInvitation(agentService.id, to)).get - invitationFrom = link.get.from + planMessage = replyToInvitation(agentService.id, link) + invitationFrom = link.from _ <- ZIO.log(s"Invitation from $invitationFrom") encryptedMessage <- opsService.packEncrypted(planMessage, to = invitationFrom) diff --git a/mercury/models/src/main/scala/org/hyperledger/identus/mercury/DidOps.scala b/mercury/models/src/main/scala/org/hyperledger/identus/mercury/DidOps.scala index ce0c7c54dd..9c0185548c 100644 --- a/mercury/models/src/main/scala/org/hyperledger/identus/mercury/DidOps.scala +++ b/mercury/models/src/main/scala/org/hyperledger/identus/mercury/DidOps.scala @@ -10,7 +10,7 @@ import scala.util.{Failure, Success, Try} trait DidOps { def packSigned(msg: Message): URIO[DidAgent, SignedMesage] def packEncrypted(msg: Message, to: DidId): URIO[DidAgent, EncryptedMessage] - // FIXME theoretically DidAgent is not needed for packEncryptedAnon + // TODO theoretically DidAgent is not needed for packEncryptedAnon def packEncryptedAnon(msg: Message, to: DidId): URIO[DidAgent, EncryptedMessage] def unpack(str: String): URIO[DidAgent, UnpackMessage] def unpackBase64(dataBase64: String): RIO[DidAgent, UnpackMessage] = { diff --git a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/Credential.scala b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/Credential.scala deleted file mode 100644 index d49bfc4403..0000000000 --- a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/Credential.scala +++ /dev/null @@ -1,30 +0,0 @@ -/* -package org.hyperledger.identus.mercury.protocol.issuecredential -import io.circe.generic.semiauto.* -import io.circe.{Decoder, Encoder} -final case class Attribute( - name: String, - value: String, - mime_type: Option[String] = None, -) -object Attribute { - given Encoder[Attribute] = deriveEncoder[Attribute] - given Decoder[Attribute] = deriveDecoder[Attribute] -} - -/** @see - * https://github.com/hyperledger/aries-rfcs/tree/main/features/0453-issue-credential-v2#preview-credential - * @param `@type` - * @param attributes - */ -final case class CredentialPreview( - `type`: String = "https://didcomm.org/issue-credential/3.0/credential-preview", - schema_id: Option[String] = None, - attributes: Seq[Attribute] -) - -object CredentialPreview { - given Encoder[CredentialPreview] = deriveEncoder[CredentialPreview] - given Decoder[CredentialPreview] = deriveDecoder[CredentialPreview] -} - */ diff --git a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/OfferCredential.scala b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/OfferCredential.scala index c60054d9bc..faa33383a8 100644 --- a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/OfferCredential.scala +++ b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/OfferCredential.scala @@ -81,23 +81,29 @@ object OfferCredential { given Decoder[Body] = deriveDecoder[Body] } - def makeOfferToProposeCredential(msg: Message): OfferCredential = { // TODO change msg: Message to ProposeCredential - val pc: ProposeCredential = ProposeCredential.readFromMessage(msg) - - OfferCredential( - body = OfferCredential.Body( - goal_code = pc.body.goal_code, - comment = pc.body.comment, - replacement_id = None, - multiple_available = None, - credential_preview = pc.body.credential_preview.get, // FIXME .get - ), - attachments = pc.attachments, - thid = msg.thid.orElse(Some(pc.id)), - from = pc.to, - to = pc.from, - ) - } + def makeOfferToProposeCredential( + msg: Message // TODO change msg: Message to ProposeCredential + ): Either[String, OfferCredential] = + ProposeCredential.readFromMessage(msg).flatMap { pc => + pc.body.credential_preview match + case None => Left("This method expects the ProposeCredential to have a 'credential_preview' in the body") + case Some(credential_preview) => + Right( + OfferCredential( + body = OfferCredential.Body( + goal_code = pc.body.goal_code, + comment = pc.body.comment, + replacement_id = None, + multiple_available = None, + credential_preview = credential_preview, + ), + attachments = pc.attachments, + thid = msg.thid.orElse(Some(pc.id)), + from = pc.to, + to = pc.from, + ) + ) + } def readFromMessage(message: Message): Either[String, OfferCredential] = { message.body.asJson.as[OfferCredential.Body] match diff --git a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/ProposeCredential.scala b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/ProposeCredential.scala index 0b9eff9cf7..10bdd4c023 100644 --- a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/ProposeCredential.scala +++ b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/ProposeCredential.scala @@ -79,20 +79,26 @@ object ProposeCredential { given Decoder[Body] = deriveDecoder[Body] } - def readFromMessage(message: Message): ProposeCredential = { - val body = message.body.asJson.as[ProposeCredential.Body].toOption.get // TODO get - - ProposeCredential( - id = message.id, - `type` = message.piuri, - body = body, - attachments = message.attachments.getOrElse(Seq.empty), - from = message.from.get, // TODO get - to = { - assert(message.to.length == 1, "The recipient is ambiguous. Need to have only 1 recipient") // TODO return error - message.to.head - }, - ) + def readFromMessage(message: Message): Either[String, ProposeCredential] = { + message.body.asJson.as[ProposeCredential.Body] match + case Left(fail) => Left("Fail to parse ProposeCredential's body: " + fail.getMessage) + case Right(body) => + message.from match + case None => Left("ProposeCredential MUST have the sender explicit") + case Some(from) => + message.to match + case firstTo +: Seq() => + Right( + ProposeCredential( + id = message.id, + `type` = message.piuri, + body = body, + attachments = message.attachments.getOrElse(Seq.empty), + from = from, + to = firstTo, + ) + ) + case tos => Left(s"ProposeCredential MUST have only 1 recipient instead has '${tos}'") } } diff --git a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/Utils.scala b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/Utils.scala index dbc057e55a..16aa55f1eb 100644 --- a/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/Utils.scala +++ b/mercury/protocol-issue-credential/src/main/scala/org/hyperledger/identus/mercury/protocol/issuecredential/Utils.scala @@ -9,7 +9,7 @@ private trait ReadAttachmentsUtils { def attachments: Seq[AttachmentDescriptor] - // TODO this formatName shoud be type safe + // TODO this formatName should be type safe lazy val getCredentialFormatAndCredential: Seq[(String, String, Array[Byte])] = attachments .flatMap(attachment => diff --git a/mercury/protocol-issue-credential/src/test/scala/org/hyperledger/identus/mercury/protocol/anotherclasspath/UtilsCredentialSpec.scala b/mercury/protocol-issue-credential/src/test/scala/org/hyperledger/identus/mercury/protocol/anotherclasspath/UtilsCredentialSpec.scala index d8fbb4da22..cad5aa83ff 100644 --- a/mercury/protocol-issue-credential/src/test/scala/org/hyperledger/identus/mercury/protocol/anotherclasspath/UtilsCredentialSpec.scala +++ b/mercury/protocol-issue-credential/src/test/scala/org/hyperledger/identus/mercury/protocol/anotherclasspath/UtilsCredentialSpec.scala @@ -102,14 +102,15 @@ class UtilsCredentialSpec extends ZSuite { ) .makeMessage - val obj = ProposeCredential.readFromMessage(msg) - - assertEquals(obj.getCredentialFormatAndCredential.size, 1) - assertEquals( - obj.getCredentialFormatAndCredential.map(_._2), - Seq(IssueCredentialProposeFormat.Unsupported(nameCredentialType).name) - ) - assertEquals(obj.getCredential[TestCredentialType](nameCredentialType).headOption, Some(credential)) + ProposeCredential.readFromMessage(msg).map { obj => + assertEquals(obj.getCredentialFormatAndCredential.size, 1) + assertEquals( + obj.getCredentialFormatAndCredential.map(_._2), + Seq(IssueCredentialProposeFormat.Unsupported(nameCredentialType).name) + ) + assertEquals(obj.getCredential[TestCredentialType](nameCredentialType).headOption, Some(credential)) + } + } test("RequestCredential encode and decode any type of Credential into the attachments") { diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala index c7c3ea323c..6123d81785 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcPresentationRepository.scala @@ -13,7 +13,6 @@ import io.circe.parser.* import io.circe.syntax.* import org.hyperledger.identus.mercury.protocol.presentproof.* import org.hyperledger.identus.pollux.core.model.* -import org.hyperledger.identus.pollux.core.model.PresentationRecord.ProtocolState import org.hyperledger.identus.pollux.core.repository.PresentationRepository import org.hyperledger.identus.shared.db.ContextAwareTask import org.hyperledger.identus.shared.db.Implicits.* @@ -36,7 +35,7 @@ class JdbcPresentationRepository( override def updatePresentationWithCredentialsToUse( recordId: DidCommID, credentialsToUse: Option[Seq[String]], - protocolState: ProtocolState + protocolState: PresentationRecord.ProtocolState ): URIO[WalletAccessContext, Unit] = { val cxnIO = sql""" | UPDATE public.presentation_records @@ -61,7 +60,7 @@ class JdbcPresentationRepository( recordId: DidCommID, credentialsToUse: Option[Seq[String]], sdJwtClaimsToDisclose: Option[SdJwtCredentialToDisclose], - protocolState: ProtocolState + protocolState: PresentationRecord.ProtocolState ): URIO[WalletAccessContext, Unit] = { val cxnIO = sql""" @@ -88,7 +87,7 @@ class JdbcPresentationRepository( recordId: DidCommID, anoncredCredentialsToUseJsonSchemaId: Option[String], anoncredCredentialsToUse: Option[AnoncredCredentialProofs], - protocolState: ProtocolState + protocolState: PresentationRecord.ProtocolState ): URIO[WalletAccessContext, Unit] = { val cxnIO = sql""" From e617dedd962f379033ae199d40addd222bf945da Mon Sep 17 00:00:00 2001 From: shotexa Date: Wed, 19 Jun 2024 02:35:37 +0400 Subject: [PATCH 2/3] feat: add support for EcdsaSecp256k1Signature2019Proof and fix pk encoding for EddsaJcs2022Proof (#1127) Signed-off-by: Shota Jolbordi --- build.sbt | 6 + .../http/StatusListCredential.scala | 3 - ...edentialStatusListRepositoryInMemory.scala | 1 - .../JdbcCredentialStatusListRepository.scala | 1 - .../identus/pollux/vc/jwt/DidJWT.scala | 9 +- .../identus/pollux/vc/jwt/DidResolver.scala | 20 +- .../EcdsaSecp256k1VerificationKey2019.scala | 50 ++++ .../identus/pollux/vc/jwt/JsonWebKey.scala | 32 ++ .../identus/pollux/vc/jwt/Proof.scala | 280 ++++++++++-------- .../vc/jwt/VerifiableCredentialPayload.scala | 48 ++- .../vc/jwt/revocation/VCStatusList2021.scala | 7 +- .../pollux/vc/jwt/JWTVerificationTest.scala | 54 ++-- .../jwt/revocation/VCStatusList2021Spec.scala | 10 +- .../identus/shared/utils/Json.scala | 35 ++- .../credentials/IssueCredentialsSteps.kt | 2 + .../kotlin/steps/proofs/PresentProofSteps.kt | 4 +- 16 files changed, 365 insertions(+), 197 deletions(-) create mode 100644 pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/EcdsaSecp256k1VerificationKey2019.scala create mode 100644 pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/JsonWebKey.scala diff --git a/build.sbt b/build.sbt index 7d542fc056..7a5348bf34 100644 --- a/build.sbt +++ b/build.sbt @@ -122,6 +122,9 @@ lazy val D = new { val jwtCirce = "com.github.jwt-scala" %% "jwt-circe" % V.jwtCirceVersion val jsonCanonicalization: ModuleID = "io.github.erdtman" % "java-json-canonicalization" % "1.1" + val titaniumJsonLd: ModuleID = "com.apicatalog" % "titanium-json-ld" % "1.4.0" + val jakartaJson: ModuleID = "org.glassfish" % "jakarta.json" % "2.0.1" + val ironVC: ModuleID = "com.apicatalog" % "iron-verifiable-credentials" % "0.14.0" val scodecBits: ModuleID = "org.scodec" %% "scodec-bits" % "1.1.38" // https://mvnrepository.com/artifact/org.didcommx/didcomm/0.3.2 @@ -184,6 +187,9 @@ lazy val D_Shared = new { D.zioCatsInterop, D.zioPrelude, D.jsonCanonicalization, + D.titaniumJsonLd, + D.jakartaJson, + D.ironVC, D.scodecBits, ) } diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala index 216e997558..83f585511d 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/credentialstatus/controller/http/StatusListCredential.scala @@ -38,9 +38,6 @@ case class StatusListCredential( ) case class CredentialSubject( - @description(annotations.credentialSubject.id.description) - @encodedExample(annotations.credentialSubject.id.example) - id: String, @description(annotations.credentialSubject.`type`.description) @encodedExample(annotations.credentialSubject.`type`.example) `type`: String, diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala index 373d68d731..7c1881ba69 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/CredentialStatusListRepositoryInMemory.scala @@ -102,7 +102,6 @@ class CredentialStatusListRepositoryInMemory( emptyJwtCredential <- VCStatusList2021 .build( vcId = s"$statusListRegistryUrl/credential-status/$id", - slId = "", revocationData = bitString, jwtIssuer = jwtIssuer ) diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala index eced32db9c..6102088e9e 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcCredentialStatusListRepository.scala @@ -92,7 +92,6 @@ class JdbcCredentialStatusListRepository(xa: Transactor[ContextAwareTask], xb: T emptyStatusListCredential <- VCStatusList2021 .build( vcId = s"$statusListRegistryUrl/credential-status/$id", - slId = "", revocationData = bitString, jwtIssuer = jwtIssuer ) diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidJWT.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidJWT.scala index 990df782e5..683b17af60 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidJWT.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidJWT.scala @@ -11,6 +11,9 @@ import org.hyperledger.identus.shared.models.KeyId import zio.* import java.security.* +import java.security.interfaces.ECPublicKey +import java.util.Base64 +import scala.jdk.CollectionConverters.* opaque type JWT = String @@ -53,7 +56,11 @@ class ES256KSigner(privateKey: PrivateKey, keyId: Option[KeyId] = None) extends } override def generateProofForJson(payload: Json, pk: PublicKey): Task[Proof] = { - EcdsaJcs2019ProofGenerator.generateProof(payload, privateKey, pk) + val err = Throwable("Public key must be secp256k1 EC public key") + pk match + case pk: ECPublicKey => + EcdsaSecp256k1Signature2019ProofGenerator.generateProof(payload, signer, pk) + case _ => ZIO.fail(err) } override def encode(claim: Json): JWT = { diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala index 840cdd4c5f..fc5a4d8c28 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/DidResolver.scala @@ -12,7 +12,6 @@ import org.hyperledger.identus.castor.core.model.did.w3c.{ } import org.hyperledger.identus.castor.core.service.DIDService import zio.* -import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} import java.time.Instant import scala.annotation.unused @@ -79,24 +78,7 @@ case class VerificationMethod( blockchainAccountId: Option[String] = Option.empty, ethereumAddress: Option[String] = Option.empty ) -case class JsonWebKey( - alg: Option[String] = Option.empty, - crv: Option[String] = Option.empty, - e: Option[String] = Option.empty, - d: Option[String] = Option.empty, - ext: Option[Boolean] = Option.empty, - key_ops: Vector[String] = Vector.empty, - kid: Option[String] = Option.empty, - kty: String, - n: Option[String] = Option.empty, - use: Option[String] = Option.empty, - x: Option[String] = Option.empty, - y: Option[String] = Option.empty -) -object JsonWebKey { - given encoder: JsonEncoder[JsonWebKey] = DeriveJsonEncoder.gen[JsonWebKey] - given decoder: JsonDecoder[JsonWebKey] = DeriveJsonDecoder.gen[JsonWebKey] -} + case class Service(id: String, `type`: String | Seq[String], serviceEndpoint: Json) /** An adapter for translating Castor resolver to resolver defined in JWT library */ diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/EcdsaSecp256k1VerificationKey2019.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/EcdsaSecp256k1VerificationKey2019.scala new file mode 100644 index 0000000000..50fb0c60a0 --- /dev/null +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/EcdsaSecp256k1VerificationKey2019.scala @@ -0,0 +1,50 @@ +package org.hyperledger.identus.pollux.vc.jwt + +import io.circe.* +import io.circe.syntax.* + +import java.time.Instant +import java.time.ZoneOffset + +case class EcdsaSecp256k1VerificationKey2019( + publicKeyJwk: JsonWebKey, + id: Option[String] = None, + controller: Option[String] = None, + expires: Option[Instant] = None +) { + val `type`: String = "EcdsaSecp256k1VerificationKey2019" + val `@context`: Set[String] = + Set("https://w3id.org/security/v1") +} + +object EcdsaSecp256k1VerificationKey2019 { + given ecdsaSecp256k1VerificationKey2019Encoder: Encoder[EcdsaSecp256k1VerificationKey2019] = + (key: EcdsaSecp256k1VerificationKey2019) => + Json + .obj( + ("@context", key.`@context`.asJson), + ("type", key.`type`.asJson), + ("id", key.id.asJson), + ("controller", key.controller.asJson), + ("publicKeyJwk", key.publicKeyJwk.asJson.dropNullValues), + ("expires", key.expires.map(_.atOffset(ZoneOffset.UTC)).asJson) + ) + + given ecdsaSecp256k1VerificationKey2019Decoder: Decoder[EcdsaSecp256k1VerificationKey2019] = + (c: HCursor) => + for { + id <- c.downField("id").as[Option[String]] + `type` <- c.downField("type").as[String] + controller <- c.downField("controller").as[Option[String]] + publicKeyJwk <- c.downField("publicKeyJwk").as[JsonWebKey] + expires <- c.downField("expires").as[Option[Instant]] + } yield { + EcdsaSecp256k1VerificationKey2019( + id = id, + publicKeyJwk = publicKeyJwk, + controller = controller, + expires = expires + ) + } + +} diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/JsonWebKey.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/JsonWebKey.scala new file mode 100644 index 0000000000..a1c86358d7 --- /dev/null +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/JsonWebKey.scala @@ -0,0 +1,32 @@ +package org.hyperledger.identus.pollux.vc.jwt + +import io.circe.* +import io.circe.generic.semiauto.* +import io.circe.syntax.* +import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder} + +case class JsonWebKey( + alg: Option[String] = Option.empty, + crv: Option[String] = Option.empty, + e: Option[String] = Option.empty, + d: Option[String] = Option.empty, + ext: Option[Boolean] = Option.empty, + key_ops: Vector[String] = Vector.empty, + kid: Option[String] = Option.empty, + kty: String, + n: Option[String] = Option.empty, + use: Option[String] = Option.empty, + x: Option[String] = Option.empty, + y: Option[String] = Option.empty +) + +object JsonWebKey { + given jsonWebKeyEncoderCirce: Encoder[JsonWebKey] = deriveEncoder[JsonWebKey] + + given jsonWebKeyDecoderCirce: Decoder[JsonWebKey] = deriveDecoder[JsonWebKey] + + given jsonWebKeyEncoderCirceZioJson: JsonEncoder[JsonWebKey] = DeriveJsonEncoder.gen[JsonWebKey] + + given jsonWebKeyDecoderCirceZioJson: JsonDecoder[JsonWebKey] = DeriveJsonDecoder.gen[JsonWebKey] + +} diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala index 471ca1fb16..47c91132fb 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/Proof.scala @@ -1,18 +1,21 @@ package org.hyperledger.identus.pollux.vc.jwt import cats.implicits.* +import com.nimbusds.jose.{JWSAlgorithm, JWSHeader, JWSObject, Payload} import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton +import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jwt.SignedJWT import io.circe.* import io.circe.syntax.* -import org.hyperledger.identus.shared.crypto.Ed25519KeyPair +import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Ed25519PublicKey, KmpEd25519KeyOps} import org.hyperledger.identus.shared.utils.{Base64Utils, Json as JsonUtils} import scodec.bits.ByteVector import zio.* import java.security.* -import java.security.spec.X509EncodedKeySpec +import java.security.interfaces.ECPublicKey import java.time.{Instant, ZoneOffset} -import scala.util.Try +import scala.jdk.CollectionConverters.* sealed trait Proof { val id: Option[String] = None @@ -22,19 +25,22 @@ sealed trait Proof { val created: Option[Instant] = None val domain: Option[String] = None val challenge: Option[String] = None - val proofValue: String val previousProof: Option[String] = None val nonce: Option[String] = None } +trait DataIntegrityProof extends Proof { + val proofValue: String +} + object Proof { given decodeProof: Decoder[Proof] = new Decoder[Proof] { final def apply(c: HCursor): Decoder.Result[Proof] = { val decoders: List[Decoder[Proof]] = List( - Decoder[EcdsaJcs2019Proof].widen - // Note: Add another proof types here when available + Decoder[EddsaJcs2022Proof].widen, + Decoder[EcdsaSecp256k1Signature2019Proof].widen, + // Note: Add another proof types here when available ) - decoders.foldLeft( Left[DecodingFailure, Proof](DecodingFailure("Cannot decode as Proof", c.history)): Decoder.Result[Proof] ) { (acc, decoder) => @@ -44,79 +50,92 @@ object Proof { } } -object EcdsaJcs2019ProofGenerator { - private val provider = BouncyCastleProviderSingleton.getInstance - def generateProof(payload: Json, sk: PrivateKey, pk: PublicKey): Task[EcdsaJcs2019Proof] = { +object EcdsaSecp256k1Signature2019ProofGenerator { + def generateProof(payload: Json, signer: ECDSASigner, pk: ECPublicKey): Task[EcdsaSecp256k1Signature2019Proof] = { for { - canonicalizedJsonString <- ZIO.fromEither(JsonUtils.canonicalizeToJcs(payload.spaces2)) - canonicalizedJson <- ZIO.fromEither(parser.parse(canonicalizedJsonString)) - dataToSign = canonicalizedJson.noSpaces.getBytes - signature = sign(sk, dataToSign) - base58BtsEncodedSignature = MultiBaseString( - header = MultiBaseString.Header.Base58Btc, - data = ByteVector.view(signature).toBase58 - ).toMultiBaseString + dataToSign <- ZIO.fromEither(JsonUtils.canonicalizeJsonLDoRdf(payload.spaces2)) created = Instant.now() - multiKey = MultiKey(publicKeyMultibase = - Some(MultiBaseString(header = MultiBaseString.Header.Base64Url, data = Base64Utils.encodeURL(pk.getEncoded))) + header = new JWSHeader.Builder(JWSAlgorithm.ES256K) + .base64URLEncodePayload(false) + .criticalParams(Set("b64").asJava) + .build() + payload = Payload(dataToSign) + jwsObject = JWSObject(header, payload) + _ = jwsObject.sign(signer) + jws = jwsObject.serialize(true) + x = pk.getW.getAffineX.toByteArray + y = pk.getW.getAffineY.toByteArray + jwk = JsonWebKey( + kty = "EC", + crv = Some("secp256k1"), + key_ops = Vector("verify"), + x = Some(Base64Utils.encodeURL(x)), + y = Some(Base64Utils.encodeURL(y)), ) - verificationMethod = Base64Utils.createDataUrl( - multiKey.asJson.dropNullValues.noSpaces.getBytes, + ecdaSecp256k1VerificationKey2019 = EcdsaSecp256k1VerificationKey2019( + publicKeyJwk = jwk + ) + verificationMethodUrl = Base64Utils.createDataUrl( + ecdaSecp256k1VerificationKey2019.asJson.dropNullValues.noSpaces.getBytes, "application/json" ) - } yield EcdsaJcs2019Proof( - proofValue = base58BtsEncodedSignature, - maybeCreated = Some(created), - verificationMethod = verificationMethod + } yield EcdsaSecp256k1Signature2019Proof( + jws = jws, + verificationMethod = verificationMethodUrl, + created = Some(created), ) } - def verifyProof(payload: Json, proofValue: String, pk: MultiKey): Task[Boolean] = { - - val res = for { - canonicalizedJsonString <- ZIO - .fromEither(JsonUtils.canonicalizeToJcs(payload.spaces2)) - .mapError(_.getMessage) - canonicalizedJson <- ZIO - .fromEither(parser.parse(canonicalizedJsonString)) - .mapError(_.getMessage) - dataToVerify = canonicalizedJson.noSpaces.getBytes - signature <- ZIO.fromEither(MultiBaseString.fromString(proofValue).flatMap(_.getBytes)) - publicKeyBytes <- ZIO.fromEither( - pk.publicKeyMultibase.toRight("No public key provided inside MultiKey").flatMap(_.getBytes) - ) - javaPublicKey <- ZIO.fromEither(recoverPublicKey(publicKeyBytes)) - isValid = verify(javaPublicKey, signature, dataToVerify) - + def verifyProof(payload: Json, jws: String, pk: ECPublicKey): Task[Boolean] = { + for { + dataToVerify <- ZIO.fromEither(JsonUtils.canonicalizeJsonLDoRdf(payload.spaces2)) + verifier = JWTVerification.toECDSAVerifier(pk) + signedJws = SignedJWT.parse(jws) + header = signedJws.getHeader + signature = signedJws.getSignature + payload = Payload(dataToVerify) + jwsObject = new SignedJWT(header.toBase64URL, payload.toBase64URL, signature) + isValid = jwsObject.verify(verifier) } yield isValid - - res.mapError(e => Throwable(e)) } +} - private def sign(privateKey: PrivateKey, data: Array[Byte]): Array[Byte] = { +object EddsaJcs2022ProofGenerator { + private val provider = BouncyCastleProviderSingleton.getInstance + private val ed25519MultiBaseHeader: Array[Byte] = Array(-19, 1) // 0xed01 - val signer = Signature.getInstance("SHA256withECDSA", provider) - signer.initSign(privateKey) - signer.update(data) - signer.sign() + private def pkToMultiKey(pk: Ed25519PublicKey): MultiKey = { + val encoded = pk.getEncoded + val withHeader = ed25519MultiBaseHeader ++ encoded + val base58Encoded = ByteVector.view(withHeader).toBase58 + MultiKey(publicKeyMultibase = + Some( + MultiBaseString( + header = MultiBaseString.Header.Base58Btc, + data = base58Encoded + ) + ) + ) } - private def recoverPublicKey(pkBytes: Array[Byte]): Either[String, PublicKey] = { - val keyFactory = KeyFactory.getInstance("EC", provider) - val x509KeySpec = X509EncodedKeySpec(pkBytes) - Try(keyFactory.generatePublic(x509KeySpec)).toEither.left.map(_.getMessage) - } + private def multiKeytoPk(multiKey: MultiKey): Either[String, Ed25519PublicKey] = { + for { + multiBaseStr <- multiKey.publicKeyMultibase.toRight("No public key provided inside MultiKey") + bytesWithHeader <- multiBaseStr.getBytes + pkBytes <- Either.cond( + bytesWithHeader.take(2).sameElements(ed25519MultiBaseHeader), + bytesWithHeader.drop(2), + "Invalid multiBaseString header for ed25519" + ) + maybePk <- Either.cond( + pkBytes.length == 32, + KmpEd25519KeyOps.publicKeyFromEncoded(pkBytes), + "Invalid public key length, must be 32" + ) + pk <- maybePk.toEither.left.map(_.getMessage) - private def verify(publicKey: PublicKey, signature: Array[Byte], data: Array[Byte]): Boolean = { - val verifier = Signature.getInstance("SHA256withECDSA", provider) - verifier.initVerify(publicKey) - verifier.update(data) - verifier.verify(signature) + } yield pk } -} - -object EddsaJcs2022ProofGenerator { - private val provider = BouncyCastleProviderSingleton.getInstance def generateProof(payload: Json, ed25519KeyPair: Ed25519KeyPair): Task[EddsaJcs2022Proof] = { for { @@ -129,14 +148,7 @@ object EddsaJcs2022ProofGenerator { data = ByteVector.view(signature).toBase58 ).toMultiBaseString created = Instant.now() - multiKey = MultiKey(publicKeyMultibase = - Some( - MultiBaseString( - header = MultiBaseString.Header.Base64Url, - data = Base64Utils.encodeURL(ed25519KeyPair.publicKey.getEncoded) - ) - ) - ) + multiKey = pkToMultiKey(ed25519KeyPair.publicKey) verificationMethod = Base64Utils.createDataUrl( multiKey.asJson.dropNullValues.noSpaces.getBytes, "application/json" @@ -154,60 +166,102 @@ object EddsaJcs2022ProofGenerator { .mapError(ioError => ParsingFailure("Error Parsing canonicalized", ioError)) canonicalizedJson <- ZIO .fromEither(parser.parse(canonicalizedJsonString)) - // .mapError(_.getMessage) dataToVerify = canonicalizedJson.noSpaces.getBytes signature <- ZIO .fromEither(MultiBaseString.fromString(proofValue).flatMap(_.getBytes)) .mapError(error => // TODO fix RuntimeException - ParsingFailure("Error Parsing MultiBaseString", new RuntimeException("Error Parsing MultiBaseString")) + ParsingFailure(error, new RuntimeException(error)) ) - publicKeyBytes <- ZIO - .fromEither(pk.publicKeyMultibase.toRight("No public key provided inside MultiKey").flatMap(_.getBytes)) + kmmPk <- ZIO + .fromEither(multiKeytoPk(pk)) .mapError(error => // TODO fix RuntimeException ParsingFailure("Error Parsing MultiBaseString", new RuntimeException("Error Parsing MultiBaseString")) ) - javaPublicKey <- ZIO - .fromEither(recoverPublicKey(publicKeyBytes)) - .mapError(error => - // TODO fix RuntimeException - ParsingFailure("Error recoverPublicKey", new RuntimeException("Error recoverPublicKey")) - ) - isValid = verify(javaPublicKey, signature, dataToVerify) - } yield isValid - private def recoverPublicKey(pkBytes: Array[Byte]): Either[String, PublicKey] = { - val keyFactory = KeyFactory.getInstance("Ed25519", provider) - val x509KeySpec = X509EncodedKeySpec(pkBytes) - Try(keyFactory.generatePublic(x509KeySpec)).toEither.left.map(_.getMessage) - } + isValid = verify(kmmPk, signature, dataToVerify) + } yield isValid - private def verify(publicKey: PublicKey, signature: Array[Byte], data: Array[Byte]): Boolean = { - val verifier = Signature.getInstance("Ed25519", provider) - verifier.initVerify(publicKey) - verifier.update(data) - verifier.verify(signature) + private def verify(publicKey: Ed25519PublicKey, signature: Array[Byte], data: Array[Byte]): Boolean = { + publicKey.verify(data, signature).isSuccess } } -case class EcdsaJcs2019Proof(proofValue: String, verificationMethod: String, maybeCreated: Option[Instant]) - extends Proof { - override val created: Option[Instant] = maybeCreated - override val `type`: String = "DataIntegrityProof" - override val proofPurpose: String = "assertionMethod" - val cryptoSuite: String = "ecdsa-jcs-2019" -} case class EddsaJcs2022Proof(proofValue: String, verificationMethod: String, maybeCreated: Option[Instant]) - extends Proof { + extends Proof + with DataIntegrityProof { override val created: Option[Instant] = maybeCreated override val `type`: String = "DataIntegrityProof" override val proofPurpose: String = "assertionMethod" val cryptoSuite: String = "eddsa-jcs-2022" } -object ProofCodecs { - def proofEncoder[T <: Proof](cryptoSuiteValue: String): Encoder[T] = (proof: T) => +object EddsaJcs2022Proof { + given proofEncoder: Encoder[EddsaJcs2022Proof] = + DataIntegrityProofCodecs.proofEncoder[EddsaJcs2022Proof]("eddsa-jcs-2022") + + given proofDecoder: Decoder[EddsaJcs2022Proof] = DataIntegrityProofCodecs.proofDecoder[EddsaJcs2022Proof]( + (proofValue, verificationMethod, created) => EddsaJcs2022Proof(proofValue, verificationMethod, created), + "eddsa-jcs-2022" + ) +} + +case class EcdsaSecp256k1Signature2019Proof( + jws: String, + verificationMethod: String, + override val created: Option[Instant] = None, + override val challenge: Option[String] = None, + override val domain: Option[String] = None, + override val nonce: Option[String] = None +) extends Proof { + override val `type`: String = "EcdsaSecp256k1Signature2019" + override val proofPurpose: String = "assertionMethod" +} + +object EcdsaSecp256k1Signature2019Proof { + + given proofEncoder: Encoder[EcdsaSecp256k1Signature2019Proof] = + (proof: EcdsaSecp256k1Signature2019Proof) => + Json + .obj( + ("id", proof.id.asJson), + ("type", proof.`type`.asJson), + ("proofPurpose", proof.proofPurpose.asJson), + ("verificationMethod", proof.verificationMethod.asJson), + ("created", proof.created.map(_.atOffset(ZoneOffset.UTC)).asJson), + ("domain", proof.domain.asJson), + ("challenge", proof.challenge.asJson), + ("jws", proof.jws.asJson), + ("nonce", proof.nonce.asJson), + ) + + given proofDecoder: Decoder[EcdsaSecp256k1Signature2019Proof] = + (c: HCursor) => + for { + id <- c.downField("id").as[Option[String]] + `type` <- c.downField("type").as[String] + proofPurpose <- c.downField("proofPurpose").as[String] + verificationMethod <- c.downField("verificationMethod").as[String] + created <- c.downField("created").as[Option[Instant]] + domain <- c.downField("domain").as[Option[String]] + challenge <- c.downField("challenge").as[Option[String]] + jws <- c.downField("jws").as[String] + nonce <- c.downField("nonce").as[Option[String]] + } yield { + EcdsaSecp256k1Signature2019Proof( + jws = jws, + verificationMethod = verificationMethod, + created = created, + challenge = challenge, + domain = domain, + nonce = nonce + ) + } +} + +object DataIntegrityProofCodecs { + def proofEncoder[T <: DataIntegrityProof](cryptoSuiteValue: String): Encoder[T] = (proof: T) => Json.obj( ("id", proof.id.asJson), ("type", proof.`type`.asJson), @@ -222,7 +276,7 @@ object ProofCodecs { ("nonce", proof.nonce.asJson) ) - def proofDecoder[T <: Proof]( + def proofDecoder[T <: DataIntegrityProof]( createProof: (String, String, Option[Instant]) => T, cryptoSuiteValue: String ): Decoder[T] = @@ -241,19 +295,3 @@ object ProofCodecs { cryptoSuite <- c.downField("cryptoSuite").as[String] } yield createProof(proofValue, verificationMethod, created) } - -object EcdsaJcs2019Proof { - given proofEncoder: Encoder[EcdsaJcs2019Proof] = ProofCodecs.proofEncoder[EcdsaJcs2019Proof]("ecdsa-jcs-2019") - given proofDecoder: Decoder[EcdsaJcs2019Proof] = ProofCodecs.proofDecoder[EcdsaJcs2019Proof]( - (proofValue, verificationMethod, created) => EcdsaJcs2019Proof(proofValue, verificationMethod, created), - "ecdsa-jcs-2019" - ) -} - -object EddsaJcs2022Proof { - given proofEncoder: Encoder[EddsaJcs2022Proof] = ProofCodecs.proofEncoder[EddsaJcs2022Proof]("eddsa-jcs-2022") - given proofDecoder: Decoder[EddsaJcs2022Proof] = ProofCodecs.proofDecoder[EddsaJcs2022Proof]( - (proofValue, verificationMethod, created) => EddsaJcs2022Proof(proofValue, verificationMethod, created), - "eddsa-jcs-2022" - ) -} diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala index e8682e0ac3..1f6e1fd787 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/VerifiableCredentialPayload.scala @@ -9,8 +9,10 @@ import io.circe.syntax.* import org.hyperledger.identus.castor.core.model.did.VerificationRelationship import org.hyperledger.identus.pollux.vc.jwt.revocation.BitString import org.hyperledger.identus.pollux.vc.jwt.schema.{SchemaResolver, SchemaValidator} +import org.hyperledger.identus.shared.crypto.KmpSecp256k1KeyOps import org.hyperledger.identus.shared.crypto.PublicKey as ApolloPublicKey import org.hyperledger.identus.shared.http.UriResolver +import org.hyperledger.identus.shared.utils.Base64Utils import pdi.jwt.* import zio.* import zio.prelude.* @@ -20,9 +22,9 @@ import java.time.{Clock, Instant, OffsetDateTime, ZoneId} import java.time.temporal.TemporalAmount import scala.util.{Failure, Try} -//TODO: I think we should remove this code and use the DID form the castor library - +//TODO: We should remove this code and use the DID form the castor library opaque type DID = String + object DID { def apply(value: String): DID = value @@ -665,30 +667,61 @@ object CredentialVerification { vcStatusListCredJson <- ZIO .fromEither(io.circe.parser.parse(statusListString)) .mapError(err => s"Could not parse status list credential as Json string: $err") + statusListCredJsonWithoutProof = vcStatusListCredJson.hcursor.downField("proof").delete.top.get proof <- ZIO .fromEither(vcStatusListCredJson.hcursor.downField("proof").as[Proof]) .mapError(err => s"Could not extract proof from status list credential: $err") // Verify proof verified <- proof match - case EcdsaJcs2019Proof(proofValue, verificationMethod, maybeCreated) => + case EddsaJcs2022Proof(proofValue, verificationMethod, maybeCreated) => val publicKeyMultiBaseEffect = uriResolver .resolve(verificationMethod) .mapError(_.toThrowable) .flatMap { jsonResponse => - ZIO.fromEither(io.circe.parser.decode[MultiKey](jsonResponse)).mapError(_.getCause) + ZIO.fromEither(io.circe.parser.decode[MultiKey](jsonResponse)).mapError(_.fillInStackTrace) } .mapError(_.getMessage) for { publicKeyMultiBase <- publicKeyMultiBaseEffect - statusListCredJsonWithoutProof = vcStatusListCredJson.hcursor.downField("proof").delete.top.get - verified <- EcdsaJcs2019ProofGenerator + verified <- EddsaJcs2022ProofGenerator .verifyProof(statusListCredJsonWithoutProof, proofValue, publicKeyMultiBase) .mapError(_.getMessage) } yield verified + case EcdsaSecp256k1Signature2019Proof(jws, verificationMethod, _, _, _, _) => + val jwkEffect = uriResolver + .resolve(verificationMethod) + .mapError(_.toThrowable) + .flatMap { jsonResponse => + ZIO + .fromEither(io.circe.parser.decode[EcdsaSecp256k1VerificationKey2019](jsonResponse)) + .map(_.publicKeyJwk) + .mapError(_.fillInStackTrace) + } + .mapError(_.getMessage) + + for { + jwk <- jwkEffect + x <- ZIO.fromOption(jwk.x).orElseFail("Missing x coordinate in public key") + y <- ZIO.fromOption(jwk.y).orElseFail("Missing y coordinate in public key") + _ <- jwk.crv.fold(ZIO.fail("Missing crv in public key")) { crv => + if crv != "secp256k1" then ZIO.fail(s"Curve must be secp256k1, got $crv") + else ZIO.unit + } + xBytes = Base64Utils.decodeURL(x) + yBytes = Base64Utils.decodeURL(y) + ecPublicKey <- ZIO + .fromTry(KmpSecp256k1KeyOps.publicKeyFromCoordinate(xBytes, yBytes)) + .map(_.toJavaPublicKey) + .mapError(_.getMessage) + verified <- EcdsaSecp256k1Signature2019ProofGenerator + .verifyProof(statusListCredJsonWithoutProof, jws, ecPublicKey) + .mapError(_.getMessage) + } yield verified // Note: add other proof types here when available + case _ => ZIO.fail(s"Unsupported proof type - ${proof.`type`}") proofVerificationValidation = @@ -904,7 +937,8 @@ object W3CCredential { for { proof <- issuer.signer.generateProofForJson(jsonCred, issuer.publicKey) jsonProof <- proof match - case a: EcdsaJcs2019Proof => ZIO.succeed(a.asJson.dropNullValues) + case b: EcdsaSecp256k1Signature2019Proof => ZIO.succeed(b.asJson.dropNullValues) + case c: EddsaJcs2022Proof => ZIO.succeed(c.asJson.dropNullValues) verifiableCredentialWithProof = jsonCred.deepMerge(Map("proof" -> jsonProof).asJson) } yield verifiableCredentialWithProof diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala index 47682410d4..b13f032950 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021.scala @@ -19,13 +19,10 @@ class VCStatusList2021 private (val vcPayload: W3cCredentialPayload, jwtIssuer: val res = for { vcId <- ZIO.fromOption(vcPayload.maybeId).mapError(_ => DecodingError("VC id not found")) - slId <- ZIO - .fromEither(vcPayload.credentialSubject.hcursor.downField("id").as[String]) - .mapError(x => DecodingError(x.message)) purpose <- ZIO .fromEither(vcPayload.credentialSubject.hcursor.downField("statusPurpose").as[StatusPurpose]) .mapError(x => DecodingError(x.message)) - } yield VCStatusList2021.build(vcId, slId, jwtIssuer, bitString, purpose) + } yield VCStatusList2021.build(vcId, jwtIssuer, bitString, purpose) res.flatten } @@ -46,7 +43,6 @@ object VCStatusList2021 { def build( vcId: String, - slId: String, jwtIssuer: Issuer, revocationData: BitString, purpose: StatusPurpose = StatusPurpose.Revocation @@ -55,7 +51,6 @@ object VCStatusList2021 { encodedBitString <- revocationData.encoded.mapError(e => EncodingError(e.message)) } yield { val claims = JsonObject() - .add("id", slId.asJson) .add("type", "StatusList2021".asJson) .add("statusPurpose", purpose.str.asJson) .add("encodedList", encodedBitString.asJson) diff --git a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala index 2489fbc0e2..7fc365bc1f 100644 --- a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala +++ b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerificationTest.scala @@ -34,34 +34,32 @@ object JWTVerificationTest extends ZIOSpecDefault { } private val statusListCredentialString = """ - |{ - | "proof" : { - | "type" : "DataIntegrityProof", - | "proofPurpose" : "assertionMethod", - | "verificationMethod" : "data:application/json;base64,eyJAY29udGV4dCI6WyJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L211bHRpa2V5L3YxIl0sInR5cGUiOiJNdWx0aWtleSIsInB1YmxpY0tleU11bHRpYmFzZSI6InVNRll3RUFZSEtvWkl6ajBDQVFZRks0RUVBQW9EUWdBRUNYSUZsMlIxOGFtZUxELXlrU09HS1FvQ0JWYkZNNW91bGtjMnZJckp0UzRQWkJnMkxyNEQzUFdYR2xHTXB1aHdwSk84MEFpdzFXeVVHT1hONkJqSlFBPT0ifQ==", - | "created" : "2024-03-04T14:44:43.867542Z", - | "proofValue" : "zAN1rKqPFt7JayDWWD4Gu7HRsNVrgqHxMhKmYT5AE1FYD5a2zaM8G4WRPBmss9M2h3J5f56sunDFbxJVuDGB8qndknijyBcqr3", - | "cryptoSuite" : "eddsa-jcs-2022" - | }, - | "@context" : [ - | "https://www.w3.org/2018/credentials/v1", - | "https://w3id.org/vc/status-list/2021/v1" - | ], - | "type" : [ - | "VerifiableCredential", - | "StatusList2021Credential" - | ], - | "id" : "http://localhost:8085/credential-status/664382dc-9e6d-4d0c-99d1-85e2c74eb5e9", - | "issuer" : "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", - | "issuanceDate" : 1709563483, - | "credentialSubject" : { - | "id" : "", - | "type" : "StatusList2021", - | "statusPurpose" : "Revocation", - | "encodedList" : "H4sIAAAAAAAA_-3BMQ0AAAACIGf_0MbwARoAAAAAAAAAAAAAAAAAAADgbbmHB0sAQAAA" - | } - |} - |""".stripMargin + |{ + | "proof" : { + | "type" : "EcdsaSecp256k1Signature2019", + | "proofPurpose" : "assertionMethod", + | "verificationMethod" : "data:application/json;base64,eyJAY29udGV4dCI6WyJodHRwczovL3czaWQub3JnL3NlY3VyaXR5L3YxIl0sInR5cGUiOiJFY2RzYVNlY3AyNTZrMVZlcmlmaWNhdGlvbktleTIwMTkiLCJwdWJsaWNLZXlKd2siOnsiY3J2Ijoic2VjcDI1NmsxIiwia2V5X29wcyI6WyJ2ZXJpZnkiXSwia3R5IjoiRUMiLCJ4IjoiQ1hJRmwyUjE4YW1lTEQteWtTT0dLUW9DQlZiRk01b3Vsa2MydklySnRTND0iLCJ5IjoiRDJRWU5pNi1BOXoxbHhwUmpLYm9jS1NUdk5BSXNOVnNsQmpsemVnWXlVQT0ifX0=", + | "created" : "2024-06-06T22:47:27.987035Z", + | "jws" : "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFUzI1NksifQ..ERDKKRukFs3UZiBdlH-e9r3rS9n05XDaR3yh-7jtmuZhY40b1CTMELHHRRfnfTv6XJ2ROziN4dj_nU_9W8qi5Q" + | }, + | "@context" : [ + | "https://www.w3.org/2018/credentials/v1", + | "https://w3id.org/vc/status-list/2021/v1" + | ], + | "type" : [ + | "VerifiableCredential", + | "StatusList2021Credential" + | ], + | "id" : "http://localhost:8085/credential-status/575092c2-7eb0-40ae-8f41-3b499f45f3dc", + | "issuer" : "did:prism:462c4811bf61d7de25b3baf86c5d2f0609b4debe53792d297bf612269bf8593a", + | "issuanceDate" : 1717714047, + | "credentialSubject" : { + | "type" : "StatusList2021", + | "statusPurpose" : "Revocation", + | "encodedList" : "H4sIAAAAAAAA_-3BMQ0AAAACIGf_0MbwARoAAAAAAAAAAAAAAAAAAADgbbmHB0sAQAAA" + | } + |} + |""".stripMargin private def createJwtCredential(issuer: IssuerWithKey): JWT = { val jwtCredentialNbf = Instant.parse("2010-01-01T00:00:00Z") // ISSUANCE DATE diff --git a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021Spec.scala b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021Spec.scala index 8a62c5f6b1..9011b923e8 100644 --- a/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021Spec.scala +++ b/pollux/vc-jwt/src/test/scala/org/hyperledger/identus/pollux/vc/jwt/revocation/VCStatusList2021Spec.scala @@ -29,7 +29,7 @@ object VCStatusList2021Spec extends ZIOSpecDefault { for { issuer <- generateIssuer() bitString <- BitString.getInstance() - statusList <- VCStatusList2021.build(VC_ID, s"$VC_ID#list", issuer, bitString) + statusList <- VCStatusList2021.build(VC_ID, issuer, bitString) json <- statusList.toJsonWithEmbeddedProof } yield { assertTrue(json.hcursor.downField("proof").focus.isDefined) @@ -39,19 +39,19 @@ object VCStatusList2021Spec extends ZIOSpecDefault { for { issuer <- generateIssuer() bitString <- BitString.getInstance() - statusList <- VCStatusList2021.build(VC_ID, s"$VC_ID#list", issuer, bitString) + statusList <- VCStatusList2021.build(VC_ID, issuer, bitString) encodedJwtVC <- statusList.encoded jwtVCPayload <- ZIO.fromTry(JwtCredential.decodeJwt(encodedJwtVC, issuer.publicKey)) credentialSubjectKeys <- ZIO.fromOption(jwtVCPayload.credentialSubject.hcursor.keys) } yield { - assertTrue(credentialSubjectKeys.toSet == Set("id", "type", "statusPurpose", "encodedList")) + assertTrue(credentialSubjectKeys.toSet == Set("type", "statusPurpose", "encodedList")) } }, test("Generated VC is valid") { for { issuer <- generateIssuer() bitString <- BitString.getInstance() - statusList <- VCStatusList2021.build(VC_ID, s"$VC_ID#list", issuer, bitString) + statusList <- VCStatusList2021.build(VC_ID, issuer, bitString) encodedJwtVC <- statusList.encoded valid <- ZIO.succeed(JwtCredential.validateEncodedJwt(encodedJwtVC, issuer.publicKey)) } yield { @@ -63,7 +63,7 @@ object VCStatusList2021Spec extends ZIOSpecDefault { issuer <- generateIssuer() bitString <- BitString.getInstance() _ <- bitString.setRevokedInPlace(1234, true) - statusList <- VCStatusList2021.build(VC_ID, s"$VC_ID#list", issuer, bitString) + statusList <- VCStatusList2021.build(VC_ID, issuer, bitString) encodedJwtVC <- statusList.encoded decodedStatusList <- VCStatusList2021.decode(encodedJwtVC, issuer) decodedBS <- decodedStatusList.getBitString diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/utils/Json.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/utils/Json.scala index fff678af68..63ec06b278 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/utils/Json.scala +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/utils/Json.scala @@ -1,8 +1,16 @@ package org.hyperledger.identus.shared.utils +import com.apicatalog.jsonld.document.JsonDocument +import com.apicatalog.jsonld.http.media.MediaType +import com.apicatalog.jsonld.JsonLd +import com.apicatalog.rdf.Rdf +import io.setl.rdf.normalization.RdfNormalize import org.erdtman.jcs.JsonCanonicalizer +import java.io.{ByteArrayInputStream, StringWriter} import java.io.IOException +import java.nio.charset.StandardCharsets +import scala.util.Try object Json { @@ -13,8 +21,29 @@ object Json { * @return * canonicalized JSON string */ - def canonicalizeToJcs(jsonStr: String): Either[IOException, String] = - try { Right(new JsonCanonicalizer(jsonStr).getEncodedString) } - catch case exception: IOException => Left(exception) + try { + Right(new JsonCanonicalizer(jsonStr).getEncodedString) + } catch case exception: IOException => Left(exception) + + /** Canonicalizes a JSON-LD string to RDF according to the Universal RDF Dataset Normalization Algorithm 2015 + * + * @param jsonLdStr + * JSON-LD string to canonicalize + * @return + * canonicalized RDF as a byte array + */ + def canonicalizeJsonLDoRdf(jsonLdStr: String): Either[Throwable, Array[Byte]] = { + Try { + val inputStream = new ByteArrayInputStream(jsonLdStr.getBytes) + val document = JsonDocument.of(inputStream) + val rdfDataset = JsonLd.toRdf(document).get + val normalized = RdfNormalize.normalize(rdfDataset) + val writer = new StringWriter + val rdfWriter = Rdf.createWriter(MediaType.N_QUADS, writer) + rdfWriter.write(normalized) + val bytes = writer.toString.getBytes(StandardCharsets.UTF_8); + bytes + }.toEither + } } diff --git a/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt b/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt index caa5a14c55..147e5989cf 100644 --- a/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/credentials/IssueCredentialsSteps.kt @@ -13,6 +13,7 @@ import net.serenitybdd.rest.SerenityRest import net.serenitybdd.screenplay.Actor import org.apache.http.HttpStatus.* import org.hyperledger.identus.client.models.* +import kotlin.time.Duration.Companion.seconds class IssueCredentialsSteps { @@ -208,6 +209,7 @@ class IssueCredentialsSteps { ) Wait.until( + 10.seconds, errorMessage = "Issuer was unable to issue the credential! " + "Protocol state did not achieve ${IssueCredentialRecord.ProtocolState.CREDENTIAL_SENT} state.", ) { diff --git a/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt b/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt index 9d2d09665d..d6452a45e8 100644 --- a/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt +++ b/tests/integration-tests/src/test/kotlin/steps/proofs/PresentProofSteps.kt @@ -103,7 +103,7 @@ class PresentProofSteps { @Then("{actor} has the proof verified") fun faberHasTheProofVerified(faber: Actor) { Wait.until( - timeout = 30.seconds, + timeout = 60.seconds, errorMessage = "Presentation did not achieve PresentationVerified state!", ) { val proofEvent = ListenToEvents.with(faber).presentationEvents.lastOrNull { @@ -116,7 +116,7 @@ class PresentProofSteps { @Then("{actor} sees the proof returned verification failed") fun verifierSeesTheProofReturnedVerificationFailed(verifier: Actor) { Wait.until( - timeout = 60.seconds, + timeout = 120.seconds, errorMessage = "Presentation did not achieve PresentationVerificationFailed state!", ) { val proofEvent = ListenToEvents.with(verifier).presentationEvents.lastOrNull { From 833d6f44ed3c152b11156c0d6814095fb0255ac0 Mon Sep 17 00:00:00 2001 From: Yurii Shynbuiev - IOHK Date: Wed, 19 Jun 2024 23:25:46 +0700 Subject: [PATCH 3/3] ci: automerge the changes from the main to the feature branch using squash commit [skip ci] (#1198) Signed-off-by: Yurii Shynbuiev --- .github/workflows/auto-merge.yml | 12 ++++++++---- .github/workflows/ff-merge.yml | 33 -------------------------------- 2 files changed, 8 insertions(+), 37 deletions(-) delete mode 100644 .github/workflows/ff-merge.yml diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml index 6a65d3b20c..179ad81397 100644 --- a/.github/workflows/auto-merge.yml +++ b/.github/workflows/auto-merge.yml @@ -4,7 +4,7 @@ name: Auto Merge Main into Feature Branch on: pull_request: - types: [ opened, synchronize, reopened, labeled ] + types: [opened, synchronize, reopened, labeled] jobs: auto-merge: @@ -34,13 +34,17 @@ jobs: git config --global user.email '${{ steps.import_gpg.outputs.email }}' - name: Fetch all branches - run: git fetch --all + run: git fetch origin - name: Checkout the feature branch run: git checkout ${{ github.event.pull_request.head.ref }} - name: Merge main into feature branch - run: git merge origin/main + run: git merge --squash origin/main + + - name: Commit changes + run: git commit -S -s -m "Merge main into ${{ github.event.pull_request.head.ref }}" + if: success() - name: Push changes run: git push origin ${{ github.event.pull_request.head.ref }} @@ -72,4 +76,4 @@ jobs: repo: context.repo.repo, issue_number: prNumber, body: 'The main branch cannot be merged into the feature branch without your help :cry:' - }); + }); diff --git a/.github/workflows/ff-merge.yml b/.github/workflows/ff-merge.yml deleted file mode 100644 index 38c9ac1c97..0000000000 --- a/.github/workflows/ff-merge.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Fast-forward merge - -# This workflow helps to work around the bug in the **Rebase and Merge** pull requests strategy. -# The bug leads to unsigned commits and prevents using the strategy -# without IOG policy violation (all commits must be signed by PGP signature). -# -# This workflow helps to merge multiple commits from PR to main branch of the repository -# without loosing of PGP signature. -# -# Related GitHub discussions: -# https://github.com/community/community/discussions/10410 -# https://github.com/orgs/community/discussions/5524 - -on: - issue_comment: - types: [created] - -jobs: - fast_forward_job: - name: Fast Forward Merge - runs-on: self-hosted - if: | - github.event.issue.pull_request != '' && - contains(github.event.comment.body, '/fast-forward') - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Fast Forward Merge - uses: endre-spotlab/fast-forward-js-action@2.1 - with: - GITHUB_TOKEN: ${{ secrets.ATALA_GITHUB_TOKEN }} - success_message: "Success! Fast forwarded ***target_base*** to ***source_head***! ```git checkout target_base && git merge source_head --ff-only``` " - failure_message: "Failed! Cannot do fast forward!"