From 46313eb3a1d27e2c59eab3741e203b7b96e582a6 Mon Sep 17 00:00:00 2001 From: mineme0110 Date: Mon, 20 May 2024 13:04:30 +0100 Subject: [PATCH] WIP progress Signed-off-by: mineme0110 --- .../identus/controller/http/ManagedDID.scala | 12 +- .../http/RequestPresentationInput.scala | 1 + .../core/service/CredentialServiceImpl.scala | 63 ++++-- .../identus/pollux/sdjwt/SDJWT.scala | 21 +- .../identus/pollux/sdjwt/SDJWTSpec.scala | 4 + .../identus/pollux/vc/jwt/DidJWT.scala | 37 +++- .../identus/pollux/vc/jwt/Proof.scala | 181 ++++++++++++++---- .../vc/jwt/VerifiableCredentialPayload.scala | 10 +- .../identus/shared/crypto/Apollo.scala | 25 ++- .../identus/shared/crypto/KmpApollo.scala | 4 +- 10 files changed, 291 insertions(+), 67 deletions(-) diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/controller/http/ManagedDID.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/controller/http/ManagedDID.scala index 021275998a..1752a0322c 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/controller/http/ManagedDID.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/controller/http/ManagedDID.scala @@ -203,14 +203,10 @@ final case class ManagedDIDKeyTemplate( @description(ManagedDIDKeyTemplate.annotations.purpose.description) @encodedExample(ManagedDIDKeyTemplate.annotations.purpose.example) purpose: Purpose, - // @description(ManagedDIDKeyTemplate.annotations.curve.description) - // @encodedExample(ManagedDIDKeyTemplate.annotations.curve.example) - // curve: Option[Curve] -) { - // TODO: this curve option is hidden for now, to be added back after integration test with node - def curve: Option[Curve] = None -} - + @description(ManagedDIDKeyTemplate.annotations.curve.description) + @encodedExample(ManagedDIDKeyTemplate.annotations.curve.example) + curve: Option[Curve] +) object ManagedDIDKeyTemplate { object annotations { object id diff --git a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/RequestPresentationInput.scala b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/RequestPresentationInput.scala index 239925f999..5d6e94211a 100644 --- a/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/RequestPresentationInput.scala +++ b/cloud-agent/service/server/src/main/scala/org/hyperledger/identus/presentproof/controller/http/RequestPresentationInput.scala @@ -101,6 +101,7 @@ object RequestPresentationInput { validator = Validator.enumeration( List( Some("JWT"), + Some("vc+sd-jwt"), Some("AnonCreds") ) ) 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 0dd0920946..ac87701741 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 @@ -1,5 +1,6 @@ package org.hyperledger.identus.pollux.core.service +import com.nimbusds.jose.jwk.OctetKeyPair import io.circe.Json import io.circe.syntax.* import org.hyperledger.identus.agent.walletapi.model.{ManagedDIDState, PublicationState} @@ -32,6 +33,7 @@ import org.hyperledger.identus.shared.utils.aspects.CustomMetricsAspect import zio.* import zio.prelude.ZValidation import org.hyperledger.identus.castor.core.model.did.EllipticCurve + import java.net.URI import java.rmi.UnexpectedException import java.time.{Instant, ZoneId} @@ -39,6 +41,7 @@ import java.util.UUID import scala.language.implicitConversions import org.hyperledger.identus.pollux.sdjwt import org.hyperledger.identus.pollux.sdjwt.* +import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Ed25519PublicKey} object CredentialServiceImpl { val layer: URLayer[ @@ -458,6 +461,17 @@ private class CredentialServiceImpl( s"${record.id}_issuance_flow_holder_req_pending_to_generated" ) } yield count + case (CredentialFormat.SDJWT, Some(subjectId)) => + for { + _ <- ZIO + .fromEither(PrismDID.fromString(subjectId)) + .mapError(_ => CredentialServiceError.UnsupportedDidFormat(subjectId)) + count <- credentialRepository + .updateWithSubjectId(recordId, subjectId, ProtocolState.RequestPending) + .mapError(RepositoryError.apply) @@ CustomMetricsAspect.startRecordingTime( + s"${record.id}_issuance_flow_holder_req_pending_to_generated" + ) + } yield count case (CredentialFormat.AnonCreds, None) => credentialRepository .updateCredentialRecordProtocolState(recordId, ProtocolState.OfferReceived, ProtocolState.RequestPending) @@ -548,13 +562,12 @@ private class CredentialServiceImpl( ) } yield jwtIssuer } - private[this] def getSDJwtIssuerKeys( + + private[this] def getEd25519SigningKeyPair( jwtIssuerDID: PrismDID, verificationRelationship: VerificationRelationship ) = { - import org.hyperledger.identus.shared.crypto.Ed25519KeyPair for { - // Automatically infer keyId to use by resolving DID and choose the corresponding VerificationRelationship issuingKeyId <- didService .resolveDID(jwtIssuerDID) .mapError(e => UnexpectedError(s"Error occured while resolving Issuing DID during VC creation: ${e.toString}")) @@ -563,14 +576,43 @@ private class CredentialServiceImpl( .someOrFail( UnexpectedError(s"Issuing DID doesn't have a key in ${verificationRelationship.name} to use: $jwtIssuerDID") ) - ecKeyPair <- managedDIDService + ed25519keyPair <- managedDIDService .findDIDKeyPair(jwtIssuerDID.asCanonical, issuingKeyId) .map(_.collect { case keyPair: Ed25519KeyPair => keyPair }) .mapError(e => UnexpectedError(s"Error occurred while getting issuer key-pair: ${e.toString}")) .someOrFail( UnexpectedError(s"Issuer key-pair does not exist in the wallet: ${jwtIssuerDID.toString}#$issuingKeyId") ) - } yield ecKeyPair + } yield ed25519keyPair + } + + /** @param jwtIssuerDID + * This can holder prism did / issuer prism did + * @param verificationRelationship + * Holder it Authentication and Issuer it is AssertionMethod + * @return + * JwtIssuer + * @see + * org.hyperledger.identus.pollux.vc.jwt.Issuer + */ + private[this] def getSDJwtIssuer( + jwtIssuerDID: PrismDID, + verificationRelationship: VerificationRelationship + ): ZIO[WalletAccessContext, UnexpectedError, JwtIssuer] = { + for { + ed25519keyPair <- getEd25519SigningKeyPair(jwtIssuerDID, verificationRelationship) + } yield { + + val d = java.util.Base64.getUrlEncoder.withoutPadding().encodeToString(ed25519keyPair.privateKey.getEncoded) + val x = java.util.Base64.getUrlEncoder.withoutPadding().encodeToString(ed25519keyPair.publicKey.getEncoded) + val okpJson = s"""{"kty":"OKP","crv":"Ed25519","d":"$d","x":"$x"}""" + val octetKeyPair = OctetKeyPair.parse(okpJson) + JwtIssuer( + org.hyperledger.identus.pollux.vc.jwt.DID(jwtIssuerDID.toString), + EdSigner(ed25519keyPair), + Ed25519PublicKey.toJavaPublicKey(ed25519keyPair.publicKey) + ) + } } override def generateJWTCredentialRequest( @@ -611,11 +653,11 @@ private class CredentialServiceImpl( } yield record } + // Holder requesting the credential override def generateSDJWTCredentialRequest( recordId: DidCommID ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = { for { - // TODO similar logic generateJWTCredentialRequest record <- getRecordWithState(recordId, ProtocolState.RequestPending) subjectId <- ZIO .fromOption(record.subjectId) @@ -624,7 +666,7 @@ private class CredentialServiceImpl( .fromEither(PrismDID.fromString(subjectId)) .mapError(_ => CredentialServiceError.UnsupportedDidFormat(subjectId)) longFormPrismDID <- getLongForm(subjectDID, true).mapError(err => UnexpectedError(err.getMessage)) - jwtIssuer <- createJwtIssuer(longFormPrismDID, VerificationRelationship.Authentication) + jwtIssuer <- getSDJwtIssuer(longFormPrismDID, VerificationRelationship.Authentication) presentationPayload <- createPresentationPayload(record, jwtIssuer) signedPayload = JwtPresentation.encodeJwt(presentationPayload.toJwtPresentationPayload, jwtIssuer) formatAndOffer <- ZIO @@ -1278,7 +1320,7 @@ private class CredentialServiceImpl( record <- markCredentialGenerated(record, issueCredential) } yield record } - + // Issuer Generating the credential override def generateSDJWTCredential( recordId: DidCommID, ): ZIO[WalletAccessContext, CredentialServiceError, IssueCredentialRecord] = { @@ -1308,7 +1350,7 @@ private class CredentialServiceImpl( .mapError(t => RepositoryError(t)), payload => ZIO.logInfo("JWT Presentation Validation Successful!") ) - jwtIssuerKey <- getSDJwtIssuerKeys(longFormPrismDID, VerificationRelationship.AssertionMethod) + ed25519KeyPair <- getEd25519SigningKeyPair(longFormPrismDID, VerificationRelationship.AssertionMethod) offerCredentialData <- ZIO .fromOption(record.offerCredentialData) .mapError(_ => @@ -1318,8 +1360,7 @@ private class CredentialServiceImpl( ) preview = offerCredentialData.body.credential_preview claims <- CredentialService.convertAttributesToJsonClaims(preview.body.attributes) - - sdJwtPrivateKey = sdjwt.IssuerPrivateKey(jwtIssuerKey.privateKey) + sdJwtPrivateKey = sdjwt.IssuerPrivateKey(ed25519KeyPair.privateKey) credential = SDJWT.issueCredential(sdJwtPrivateKey, claims.asJson.noSpaces, ???) // FIXME issueCredential = IssueCredential.build( diff --git a/pollux/sd-jwt/src/main/scala/org/hyperledger/identus/pollux/sdjwt/SDJWT.scala b/pollux/sd-jwt/src/main/scala/org/hyperledger/identus/pollux/sdjwt/SDJWT.scala index defb59aed6..add5c08e5b 100644 --- a/pollux/sd-jwt/src/main/scala/org/hyperledger/identus/pollux/sdjwt/SDJWT.scala +++ b/pollux/sd-jwt/src/main/scala/org/hyperledger/identus/pollux/sdjwt/SDJWT.scala @@ -9,8 +9,7 @@ import org.bouncycastle.crypto.util.SubjectPublicKeyInfoFactory import scala.util.Try import scala.util.Failure import scala.util.Success -import org.bouncycastle.cert.path.CertPathValidationResult - +import zio.json.* // TODO move to apollo private[sdjwt] object Utils { @@ -134,12 +133,28 @@ object PresentationJson { } object SDJWT { - + given encoder: JsonEncoder[String | Int] = new JsonEncoder[String | Int] { + override def unsafeEncode(b: String | Int, indent: Option[Int], out: zio.json.internal.Write): Unit = { + b match { + case obj: String => JsonEncoder.string.unsafeEncode(obj, indent, out) + case obj: Int => JsonEncoder.int.unsafeEncode(obj, indent, out) + } + } + } def issueCredential( issueKey: IssuerPrivateKey, claims: String, ): CredentialJson = issueCredential(issueKey, claims, None) + def issueCredential( + issueKey: IssuerPrivateKey, + claimsMap: Map[String, String], + ): CredentialJson = { + val claims = claimsMap ++ + Map("sub" -> "did:example:holder", "iss" -> "did:example:issuer", "iat" -> 1683000000, "exp" -> 1883000000) + issueCredential(issueKey, claims.toJson, None) + } + def issueCredential( issueKey: IssuerPrivateKey, claims: String, diff --git a/pollux/sd-jwt/src/test/scala/org/hyperledger/identus/pollux/sdjwt/SDJWTSpec.scala b/pollux/sd-jwt/src/test/scala/org/hyperledger/identus/pollux/sdjwt/SDJWTSpec.scala index b96af06937..d09f9d3cc2 100644 --- a/pollux/sd-jwt/src/test/scala/org/hyperledger/identus/pollux/sdjwt/SDJWTSpec.scala +++ b/pollux/sd-jwt/src/test/scala/org/hyperledger/identus/pollux/sdjwt/SDJWTSpec.scala @@ -109,6 +109,10 @@ object SDJWTSpec extends ZIOSpecDefault { test("make presentation") { val credential = SDJWT.issueCredential(ISSUER_KEY, CLAIMS) val presentation = SDJWT.createPresentation(credential, CLAIMS_PRESENTED) + println("**************") + println(s"${presentation.value}") + println("**************") + assertTrue(!presentation.value.isEmpty()) }, test("verify presentation") { 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 19bf145f9d..8a24837547 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 @@ -1,16 +1,16 @@ package org.hyperledger.identus.pollux.vc.jwt -import com.nimbusds.jose.{JWSAlgorithm, JWSHeader} -import com.nimbusds.jose.crypto.ECDSASigner +import com.nimbusds.jose.crypto.{ECDSASigner, Ed25519Signer} import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton -import com.nimbusds.jose.jwk.{Curve, ECKey} +import com.nimbusds.jose.jwk.{Curve, ECKey, OctetKeyPair} +import com.nimbusds.jose.{JWSAlgorithm, JWSHeader} import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT} import io.circe.* +import org.hyperledger.identus.shared.crypto.Ed25519KeyPair import zio.* -import pdi.jwt.algorithms.JwtECDSAAlgorithm -import pdi.jwt.{JwtAlgorithm, JwtCirce} import java.security.* +import java.util.Base64 opaque type JWT = String @@ -40,7 +40,7 @@ class ES256KSigner(privateKey: PrivateKey) extends Signer { } override def generateProofForJson(payload: Json, pk: PublicKey): Task[Proof] = { - EddsaJcs2022ProofGenerator.generateProof(payload, privateKey, pk) + EcdsaJcs2019ProofGenerator.generateProof(payload, privateKey, pk) } override def encode(claim: Json): JWT = { @@ -54,6 +54,31 @@ class ES256KSigner(privateKey: PrivateKey) extends Signer { } } +class EdSigner(ed25519KeyPair: Ed25519KeyPair) extends Signer { + lazy val signer: Ed25519Signer = { + val d = java.util.Base64.getUrlEncoder.withoutPadding().encodeToString(ed25519KeyPair.privateKey.getEncoded) + val x = java.util.Base64.getUrlEncoder.withoutPadding().encodeToString(ed25519KeyPair.publicKey.getEncoded) + val okpJson = s"""{"kty":"OKP","crv":"Ed25519","d":"$d","x":"$x"}""" + val octetKeyPair = OctetKeyPair.parse(okpJson) + val ed25519Signer = Ed25519Signer(octetKeyPair) + ed25519Signer + } + + override def generateProofForJson(payload: Json, pk: PublicKey): Task[Proof] = { + EddsaJcs2022ProofGenerator.generateProof(payload, ed25519KeyPair) + } + + override def encode(claim: Json): JWT = { + val claimSet = JWTClaimsSet.parse(claim.noSpaces) + val signedJwt = SignedJWT( + new JWSHeader.Builder(JWSAlgorithm.EdDSA).build(), + claimSet + ) + signedJwt.sign(signer) + JWT(signedJwt.serialize()) + } +} + def toJWKFormat(holderJwk: ECKey): JsonWebKey = { JsonWebKey( kty = "EC", 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 c27c6999ba..edb7eadea9 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 @@ -12,7 +12,7 @@ import scodec.bits.ByteVector import scala.util.Try import java.security.* import java.security.spec.X509EncodedKeySpec - +import org.hyperledger.identus.shared.crypto.Ed25519KeyPair sealed trait Proof { val id: Option[String] = None val `type`: String @@ -30,7 +30,7 @@ object Proof { given decodeProof: Decoder[Proof] = new Decoder[Proof] { final def apply(c: HCursor): Decoder.Result[Proof] = { val decoders: List[Decoder[Proof]] = List( - Decoder[EddsaJcs2022Proof].widen + Decoder[EcdsaJcs2019Proof].widen // Note: Add another proof types here when available ) @@ -43,9 +43,9 @@ object Proof { } } -object EddsaJcs2022ProofGenerator { +object EcdsaJcs2019ProofGenerator { private val provider = BouncyCastleProviderSingleton.getInstance - def generateProof(payload: Json, sk: PrivateKey, pk: PublicKey): Task[EddsaJcs2022Proof] = { + def generateProof(payload: Json, sk: PrivateKey, pk: PublicKey): Task[EcdsaJcs2019Proof] = { for { canonicalizedJsonString <- ZIO.fromEither(JsonUtils.canonicalizeToJcs(payload.spaces2)) canonicalizedJson <- ZIO.fromEither(parser.parse(canonicalizedJsonString)) @@ -63,7 +63,7 @@ object EddsaJcs2022ProofGenerator { multiKey.asJson.dropNullValues.noSpaces.getBytes, "application/json" ) - } yield EddsaJcs2022Proof( + } yield EcdsaJcs2019Proof( proofValue = base58BtsEncodedSignature, maybeCreated = Some(created), verificationMethod = verificationMethod @@ -113,6 +113,114 @@ object EddsaJcs2022ProofGenerator { verifier.verify(signature) } } + +object EddsaJcs2022ProofGenerator { + private val provider = BouncyCastleProviderSingleton.getInstance + def generateProof1(payload: Json, sk: PrivateKey, pk: PublicKey): Task[EcdsaJcs2019Proof] = { + 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 + created = Instant.now() + multiKey = MultiKey(publicKeyMultibase = + Some(MultiBaseString(header = MultiBaseString.Header.Base64Url, data = Base64Utils.encodeURL(pk.getEncoded))) + ) + verificationMethod = Base64Utils.createDataUrl( + multiKey.asJson.dropNullValues.noSpaces.getBytes, + "application/json" + ) + } yield EcdsaJcs2019Proof( + proofValue = base58BtsEncodedSignature, + maybeCreated = Some(created), + verificationMethod = verificationMethod + ) + } + def generateProof(payload: Json, ed25519KeyPair: Ed25519KeyPair): Task[EcdsaJcs2019Proof] = { + for { + canonicalizedJsonString <- ZIO.fromEither(JsonUtils.canonicalizeToJcs(payload.spaces2)) + canonicalizedJson <- ZIO.fromEither(parser.parse(canonicalizedJsonString)) + dataToSign = canonicalizedJson.noSpaces.getBytes + signature = ed25519KeyPair.privateKey.sign(dataToSign) + base58BtsEncodedSignature = MultiBaseString( + header = MultiBaseString.Header.Base58Btc, + data = ByteVector.view(signature).toBase58 + ).toMultiBaseString + created = Instant.now() + multiKey = MultiKey(publicKeyMultibase = + Some( + MultiBaseString( + header = MultiBaseString.Header.Base64Url, + data = Base64Utils.encodeURL(ed25519KeyPair.publicKey.getEncoded) + ) + ) + ) + verificationMethod = Base64Utils.createDataUrl( + multiKey.asJson.dropNullValues.noSpaces.getBytes, + "application/json" + ) + } yield EcdsaJcs2019Proof( + proofValue = base58BtsEncodedSignature, + maybeCreated = Some(created), + verificationMethod = verificationMethod + ) + } + + 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) + + } yield isValid + + res.mapError(e => Throwable(e)) + } + + private def sign(privateKey: PrivateKey, data: Array[Byte]): Array[Byte] = { + + val signer = Signature.getInstance("Ed25519", provider) + signer.initSign(privateKey) + signer.update(data) + signer.sign() + } + + private def recoverPublicKey(pkBytes: Array[Byte]): Either[String, PublicKey] = { + val keyFactory = KeyFactory.getInstance("EdDSA", provider) + val x509KeySpec = X509EncodedKeySpec(pkBytes) + Try(keyFactory.generatePublic(x509KeySpec)).toEither.left.map(_.getMessage) + } + + 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) + } +} +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 { override val created: Option[Instant] = maybeCreated @@ -121,27 +229,26 @@ case class EddsaJcs2022Proof(proofValue: String, verificationMethod: String, may val cryptoSuite: String = "eddsa-jcs-2022" } -object EddsaJcs2022Proof { - - given proofEncoder: Encoder[EddsaJcs2022Proof] = - (proof: EddsaJcs2022Proof) => - 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), - ("proofValue", proof.proofValue.asJson), - ("cryptoSuite", proof.cryptoSuite.asJson), - ("previousProof", proof.previousProof.asJson), - ("nonce", proof.nonce.asJson), - ("cryptoSuite", proof.cryptoSuite.asJson), - ) +object ProofCodecs { + def proofEncoder[T <: Proof](cryptoSuiteValue: String): Encoder[T] = (proof: T) => + 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), + ("proofValue", proof.proofValue.asJson), + ("cryptoSuite", Json.fromString(cryptoSuiteValue)), + ("previousProof", proof.previousProof.asJson), + ("nonce", proof.nonce.asJson) + ) - given proofDecoder: Decoder[EddsaJcs2022Proof] = + def proofDecoder[T <: Proof]( + createProof: (String, String, Option[Instant]) => T, + cryptoSuiteValue: String + ): Decoder[T] = (c: HCursor) => for { id <- c.downField("id").as[Option[String]] @@ -155,11 +262,21 @@ object EddsaJcs2022Proof { previousProof <- c.downField("previousProof").as[Option[String]] nonce <- c.downField("nonce").as[Option[String]] cryptoSuite <- c.downField("cryptoSuite").as[String] - } yield { - EddsaJcs2022Proof( - proofValue = proofValue, - verificationMethod = verificationMethod, - maybeCreated = created - ) - } + } 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 445b707dd7..73d94fb3ed 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 @@ -19,7 +19,7 @@ import java.time.{Clock, Instant, OffsetDateTime, ZoneId} import scala.util.Try import com.nimbusds.jwt.SignedJWT import scala.util.Failure - +import org.hyperledger.identus.shared.crypto.{PublicKey => ApolloPublicKey} opaque type DID = String object DID { def apply(value: String): DID = value @@ -31,6 +31,8 @@ object DID { case class Issuer(did: DID, signer: Signer, publicKey: PublicKey) +case class ApolloIssuer(did: DID, signer: Signer, publicKey: ApolloPublicKey) + sealed trait VerifiableCredentialPayload case class W3cVerifiableCredentialPayload(payload: W3cCredentialPayload, proof: JwtProof) @@ -667,7 +669,7 @@ object CredentialVerification { // Verify proof verified <- proof match - case EddsaJcs2022Proof(proofValue, verificationMethod, maybeCreated) => + case EcdsaJcs2019Proof(proofValue, verificationMethod, maybeCreated) => val publicKeyMultiBaseEffect = uriResolver .resolve(verificationMethod) .mapError(_.toThrowable) @@ -679,7 +681,7 @@ object CredentialVerification { for { publicKeyMultiBase <- publicKeyMultiBaseEffect statusListCredJsonWithoutProof = vcStatusListCredJson.hcursor.downField("proof").delete.top.get - verified <- EddsaJcs2022ProofGenerator + verified <- EcdsaJcs2019ProofGenerator .verifyProof(statusListCredJsonWithoutProof, proofValue, publicKeyMultiBase) .mapError(_.getMessage) } yield verified @@ -900,7 +902,7 @@ object W3CCredential { for { proof <- issuer.signer.generateProofForJson(jsonCred, issuer.publicKey) jsonProof <- proof match - case a: EddsaJcs2022Proof => ZIO.succeed(a.asJson.dropNullValues) + case a: EcdsaJcs2019Proof => ZIO.succeed(a.asJson.dropNullValues) verifiableCredentialWithProof = jsonCred.deepMerge(Map("proof" -> jsonProof).asJson) } yield verifiableCredentialWithProof diff --git a/shared/crypto/src/main/scala/org/hyperledger/identus/shared/crypto/Apollo.scala b/shared/crypto/src/main/scala/org/hyperledger/identus/shared/crypto/Apollo.scala index 304eef02a5..d36077c284 100644 --- a/shared/crypto/src/main/scala/org/hyperledger/identus/shared/crypto/Apollo.scala +++ b/shared/crypto/src/main/scala/org/hyperledger/identus/shared/crypto/Apollo.scala @@ -1,13 +1,21 @@ package org.hyperledger.identus.shared.crypto +import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import org.hyperledger.identus.shared.models.HexString import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.spec.ECNamedCurveSpec import zio.* -import java.security.KeyFactory -import java.security.spec.{ECPrivateKeySpec, ECPublicKeySpec} +import java.security.{KeyFactory, PublicKey} +import java.security.spec.{ + ECPrivateKeySpec, + ECPublicKeySpec, + EdECPoint, + EdECPublicKeySpec, + NamedParameterSpec, + X509EncodedKeySpec +} import scala.util.Try trait Apollo { @@ -52,6 +60,8 @@ enum DerivationPath { final case class ECPoint(x: Array[Byte], y: Array[Byte]) +final case class EdECPoint(x: Boolean, y: Array[Byte]) + // secp256k1 final case class Secp256k1KeyPair(publicKey: Secp256k1PublicKey, privateKey: Secp256k1PrivateKey) trait Secp256k1PublicKey extends PublicKey, Verifiable { @@ -121,6 +131,14 @@ trait Secp256k1KeyOps { // ed25519 final case class Ed25519KeyPair(publicKey: Ed25519PublicKey, privateKey: Ed25519PrivateKey) +object Ed25519PublicKey { + def toJavaPublicKey(publicKey: Ed25519PublicKey): java.security.PublicKey = { + val keySpec: X509EncodedKeySpec = new X509EncodedKeySpec(publicKey.getEncoded) + val keyFactory = KeyFactory.getInstance("DSA", BouncyCastleProviderSingleton.getInstance) + val javaPublicKey = keyFactory.generatePublic(keySpec) + javaPublicKey + } +} trait Ed25519PublicKey extends PublicKey, Verifiable { override final def hashCode(): Int = HexString.fromByteArray(getEncoded).hashCode() @@ -129,6 +147,9 @@ trait Ed25519PublicKey extends PublicKey, Verifiable { HexString.fromByteArray(this.getEncoded) == HexString.fromByteArray(otherPK.getEncoded) case _ => false } + + // def getEdECPoint: EdECPoint + } trait Ed25519PrivateKey extends PrivateKey, Signable { type Pub = Ed25519PublicKey diff --git a/shared/crypto/src/main/scala/org/hyperledger/identus/shared/crypto/KmpApollo.scala b/shared/crypto/src/main/scala/org/hyperledger/identus/shared/crypto/KmpApollo.scala index 4d043521fd..3c2062d847 100644 --- a/shared/crypto/src/main/scala/org/hyperledger/identus/shared/crypto/KmpApollo.scala +++ b/shared/crypto/src/main/scala/org/hyperledger/identus/shared/crypto/KmpApollo.scala @@ -14,8 +14,9 @@ import io.iohk.atala.prism.apollo.utils.KMMX25519PrivateKey import io.iohk.atala.prism.apollo.utils.KMMX25519PublicKey import zio.* + import scala.jdk.CollectionConverters.* -import scala.util.{Try, Success, Failure} +import scala.util.{Failure, Success, Try} final case class KmpSecp256k1PublicKey(publicKey: KMMECSecp256k1PublicKey) extends Secp256k1PublicKey { @@ -109,6 +110,7 @@ object KmpSecp256k1KeyOps extends Secp256k1KeyOps { } final case class KmpEd25519PublicKey(publicKey: KMMEdPublicKey) extends Ed25519PublicKey { + override def getEncoded: Array[Byte] = publicKey.getRaw() override def verify(data: Array[Byte], signature: Array[Byte]): Try[Unit] = Try(publicKey.verify(data, signature))