From 1c1a171d1ba983a9c93644ded4feafe0ed6e5294 Mon Sep 17 00:00:00 2001 From: patlo-iog <108713642+patlo-iog@users.noreply.github.com> Date: Wed, 15 Feb 2023 17:38:22 +0700 Subject: [PATCH] feat(prism-agent): issue credential to Prism DID holder by Prism DID issuer (#373) * feat(prism-agent): add getKeyWithDID to ManagedDIDService * fix(prism-agent): make DIDSecretStorage private to walletapi * feat(prism-agent): add issingDID to IssueCredentialRecord in OAS * feat(prism-agent): use PrismDID isseur from DB instead of creating new one * feat(prism-agent): add connectionId to credential-offer endpoint * feat(prism-agent): separate pairwiseDID from issuingDID / subjectId * feat(prism-agent): remove merge conflict * feat(prism-agent): infer issuing key from DID document * feat(prism-agent): use Prism DID for presentation * pr cleanup * fix pr review comment --- .../service/api/http/pollux/schemas.yaml | 27 +++- prism-agent/service/issue.md | 40 +++++- .../service/project/Dependencies.scala | 2 +- .../io/iohk/atala/agent/server/Main.scala | 2 - .../io/iohk/atala/agent/server/Modules.scala | 8 +- .../server/http/marshaller/JsonSupport.scala | 4 +- .../http/model/OASErrorModelHelper.scala | 21 +-- ...sueCredentialsProtocolApiServiceImpl.scala | 128 +++++++++--------- .../agent/server/jobs/BackgroundJobs.scala | 119 +++++++++------- .../walletapi/model/error/GetKeyError.scala | 8 ++ .../walletapi/service/ManagedDIDService.scala | 65 ++++++++- .../walletapi/storage/DIDSecretStorage.scala | 2 +- 12 files changed, 286 insertions(+), 140 deletions(-) create mode 100644 prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/GetKeyError.scala diff --git a/prism-agent/service/api/http/pollux/schemas.yaml b/prism-agent/service/api/http/pollux/schemas.yaml index 3d18071b0c..5e17601846 100644 --- a/prism-agent/service/api/http/pollux/schemas.yaml +++ b/prism-agent/service/api/http/pollux/schemas.yaml @@ -228,8 +228,7 @@ components: # Issue Credential Protocol - CreateIssueCredentialRecordRequest: - description: A request to create a new "issue credential record" + IssueCredentialRecordBase: required: - subjectId - claims @@ -257,11 +256,29 @@ components: type: boolean default: true + CreateIssueCredentialRecordRequest: + description: A request to create a new "issue credential record" + type: object + allOf: + - $ref: "#/components/schemas/IssueCredentialRecordBase" + - type: object + required: + - issuingDID + - connectionId + properties: + issuingDID: + type: string + description: Issuer DID of the verifiable credentials object + example: did:prism:issuerofverifiablecredentials + connectionId: + type: string + description: A connection ID between issuer and holder. + IssueCredentialRecord: description: An issue credential record to store the state of the protocol execution type: object allOf: - - $ref: "#/components/schemas/CreateIssueCredentialRecordRequest" + - $ref: "#/components/schemas/IssueCredentialRecordBase" - type: object required: - recordId @@ -306,6 +323,10 @@ components: - Published jwtCredential: type: string + issuingDID: + type: string + description: Issuer DID of the verifiable credentials object + example: did:prism:issuerofverifiablecredentials IssueCredentialRecordCollection: description: A collection of issue credential records diff --git a/prism-agent/service/issue.md b/prism-agent/service/issue.md index ab727b85c8..b650778abd 100644 --- a/prism-agent/service/issue.md +++ b/prism-agent/service/issue.md @@ -18,9 +18,39 @@ PORT=8090 docker-compose -p holder -f infrastructure/local/docker-compose.yml up ### Executing the `Issue` flow --- +- **Issuer** - Create a DID that will be used for issuing a VC + +```bash +curl --location --request POST 'http://localhost:8080/prism-agent/did-registrar/dids' \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + --data-raw '{ + "documentTemplate": { + "publicKeys": [ + { + "id": "my-issuing-key", + "purpose": "assertionMethod" + } + ], + "services": [] + } + }' +``` + +- **Issuer** - Publish an issuing DID to the blockchain + +Replace `DID_REF` by the DID on Prism Agent that should be published +```bash +curl --location --request POST 'http://localhost:8080/prism-agent/did-registrar/dids/{DID_REF}/publications' \ +--header 'Accept: application/json' +``` + - **Issuer** - Initiate a new issue credential flow -Replace `{SUBJECT_ID}` with the DID of the holder displayed at startup in the his Prism Agent console logs +Replace `{SUBJECT_ID}` with the DID of the holder and `{CONNECTION_ID}` with the connection to the holder. +This assumes that there is a connection already established (see ["connect" documentation](./connect.md)). Also `{ISSUING_DID}` must be specified using the DID created above. + + ```bash curl -X 'POST' \ 'http://localhost:8080/prism-agent/issue-credentials/credential-offers' \ @@ -29,6 +59,8 @@ curl -X 'POST' \ -d '{ "schemaId": "schema:1234", "subjectId": "{SUBJECT_ID}", + "connectionId": "{CONNECTION_ID}", + "issuingDID": "{ISSUING_DID}", "validityPeriod": 3600, "automaticIssuance": false, "awaitConfirmation": false, @@ -37,7 +69,7 @@ curl -X 'POST' \ "lastname": "Wonderland", "birthdate": "01/01/2000" } - }' | jq + }' | jq ``` - **Holder** - Retrieving the list of issue records @@ -45,7 +77,7 @@ curl -X 'POST' \ curl -X 'GET' 'http://localhost:8090/prism-agent/issue-credentials/records' | jq ``` -- **Holder** - Accepting the credential offer +- **Holder** - Accepting the credential offer Replace `{RECORD_ID}` with the UUID of the record from the previous list ```bash @@ -62,4 +94,4 @@ curl -X 'GET' 'http://localhost:8080/prism-agent/issue-credentials/records' | jq Replace `{RECORD_ID}` with the UUID of the record from the previous list ```bash curl -X 'POST' 'http://localhost:8080/prism-agent/issue-credentials/records/{RECORD_ID}/issue-credential' | jq -``` \ No newline at end of file +``` diff --git a/prism-agent/service/project/Dependencies.scala b/prism-agent/service/project/Dependencies.scala index 39ba560ee7..38f9be0873 100644 --- a/prism-agent/service/project/Dependencies.scala +++ b/prism-agent/service/project/Dependencies.scala @@ -9,7 +9,7 @@ object Dependencies { val akka = "2.6.20" val akkaHttp = "10.2.9" val castor = "0.8.0" - val pollux = "0.26.0" + val pollux = "0.27.0" val connect = "0.10.0" val bouncyCastle = "1.70" val logback = "1.4.5" diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala index 616137bcb2..b7cbde957f 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Main.scala @@ -99,8 +99,6 @@ object Main extends ZIOAppDefault { HttpModule.layers, RepoModule.credentialSchemaServiceLayer, AppModule.manageDIDServiceLayer, - JdbcDIDSecretStorage.layer, - RepoModule.agentTransactorLayer, RepoModule.verificationPolicyServiceLayer ) } yield app diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala index 44d86421f7..bd7dedbffc 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/Modules.scala @@ -91,7 +91,6 @@ import io.circe.ParsingFailure import io.circe.DecodingFailure import io.iohk.atala.agent.walletapi.sql.{JdbcDIDNonSecretStorage, JdbcDIDSecretStorage} import io.iohk.atala.resolvers.DIDResolver -import io.iohk.atala.agent.walletapi.storage.DIDSecretStorage import io.iohk.atala.pollux.vc.jwt.DidResolver as JwtDidResolver import io.iohk.atala.pollux.vc.jwt.PrismDidResolver import io.iohk.atala.mercury.DidAgent @@ -163,8 +162,7 @@ object Modules { } val issueCredentialDidCommExchangesJob: RIO[ - AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & CredentialService & ManagedDIDService & - DIDSecretStorage, + AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & CredentialService & DIDService & ManagedDIDService, Unit ] = for { @@ -175,8 +173,8 @@ object Modules { } yield job val presentProofExchangeJob: RIO[ - AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & PresentationService & ManagedDIDService & - DIDSecretStorage, + AppConfig & DidOps & DIDResolver & JwtDidResolver & HttpClient & PresentationService & DIDService & + ManagedDIDService, Unit ] = for { diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/JsonSupport.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/JsonSupport.scala index a10b63afa7..a00b744f65 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/JsonSupport.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/marshaller/JsonSupport.scala @@ -67,8 +67,8 @@ trait JsonSupport extends SprayJsonSupport with DefaultJsonProtocol { } } } - given RootJsonFormat[CreateIssueCredentialRecordRequest] = jsonFormat6(CreateIssueCredentialRecordRequest.apply) - given RootJsonFormat[IssueCredentialRecord] = jsonFormat13(IssueCredentialRecord.apply) + given RootJsonFormat[CreateIssueCredentialRecordRequest] = jsonFormat8(CreateIssueCredentialRecordRequest.apply) + given RootJsonFormat[IssueCredentialRecord] = jsonFormat14(IssueCredentialRecord.apply) given RootJsonFormat[IssueCredentialRecordCollection] = jsonFormat4(IssueCredentialRecordCollection.apply) // diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASErrorModelHelper.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASErrorModelHelper.scala index 674aff08a6..1794c19803 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASErrorModelHelper.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/model/OASErrorModelHelper.scala @@ -27,18 +27,23 @@ trait OASErrorModelHelper { extension [E](e: HttpServiceError[E]) { def toOAS(using te: ToErrorResponse[E]): ErrorResponse = { e match - case HttpServiceError.InvalidPayload(msg) => - ErrorResponse( - `type` = "error-type", - title = "error-title", - status = 422, - detail = Some(msg), - instance = "error-instance" - ) + case e: HttpServiceError.InvalidPayload => e.toOAS case HttpServiceError.DomainError(cause) => te.toErrorResponse(cause) } } + extension (e: HttpServiceError.InvalidPayload) { + def toOAS: ErrorResponse = { + ErrorResponse( + `type` = "InvalidPayload", + title = "error-title", + status = 422, + detail = Some(e.msg), + instance = "error-instance" + ) + } + } + given ToErrorResponse[DIDOperationError] with { override def toErrorResponse(e: DIDOperationError): ErrorResponse = { ErrorResponse( diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/IssueCredentialsProtocolApiServiceImpl.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/IssueCredentialsProtocolApiServiceImpl.scala index a835471811..2421a8a8d4 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/IssueCredentialsProtocolApiServiceImpl.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/http/service/IssueCredentialsProtocolApiServiceImpl.scala @@ -24,6 +24,7 @@ import io.iohk.atala.connect.core.model.error.ConnectionServiceError import io.iohk.atala.connect.core.model.ConnectionRecord import io.iohk.atala.connect.core.model.ConnectionRecord.Role import io.iohk.atala.connect.core.model.ConnectionRecord.ProtocolState +import io.iohk.atala.castor.core.model.did.PrismDID class IssueCredentialsProtocolApiServiceImpl( credentialService: CredentialService, @@ -43,17 +44,27 @@ class IssueCredentialsProtocolApiServiceImpl( toEntityMarshallerErrorResponse: ToEntityMarshaller[ErrorResponse] ): Route = { val result = for { - didIdPair <- getPairwiseDIDs(request.subjectId) + didIdPair <- getPairwiseDIDs(request.connectionId) + issuingDID <- ZIO + .fromEither(PrismDID.fromString(request.issuingDID)) + .mapError(HttpServiceError.InvalidPayload.apply) + .mapError(_.toOAS) + subjectId <- ZIO + .fromEither(PrismDID.fromString(request.subjectId)) + .mapError(HttpServiceError.InvalidPayload.apply) + .mapError(_.toOAS) outcome <- credentialService .createIssueCredentialRecord( - pairwiseDID = didIdPair.myDID, + pairwiseIssuerDID = didIdPair.myDID, + pairwiseHolderDID = didIdPair.theirDid, thid = UUID.randomUUID(), - didIdPair.theirDid.value, - request.schemaId, - request.claims, - request.validityPeriod, - request.automaticIssuance.orElse(Some(true)), - request.awaitConfirmation.orElse(Some(false)) + subjectId = subjectId.toString, + schemaId = request.schemaId, + claims = request.claims, + validityPeriod = request.validityPeriod, + automaticIssuance = request.automaticIssuance.orElse(Some(true)), + awaitConfirmation = request.awaitConfirmation.orElse(Some(false)), + issuingDID = Some(issuingDID.asCanonical) ) .mapError(HttpServiceError.DomainError[CredentialServiceError].apply) .mapError(_.toOAS) @@ -143,60 +154,53 @@ class IssueCredentialsProtocolApiServiceImpl( } } - private[this] def getPairwiseDIDs(subjectId: String): ZIO[Any, ErrorResponse, DidIdPair] = { - val didRegex = "^did:.*".r - subjectId match { - case didRegex() => - for { - pairwiseDID <- managedDIDService.createAndStorePeerDID(agentConfig.didCommServiceEndpointUrl) - } yield DidIdPair(pairwiseDID.did, DidId(subjectId)) - case _ => - for { - maybeConnection <- connectionService - .getConnectionRecord(UUID.fromString(subjectId)) - .mapError(HttpServiceError.DomainError[ConnectionServiceError].apply) - .mapError(_.toOAS) - connection <- ZIO - .fromOption(maybeConnection) - .mapError(_ => notFoundErrorResponse(Some("Connection not found"))) - connectionResponse <- ZIO - .fromOption(connection.connectionResponse) - .mapError(_ => notFoundErrorResponse(Some("ConnectionResponse not found in record"))) - didIdPair <- connection match - case ConnectionRecord( - _, - _, - _, - _, - _, - Role.Inviter, - ProtocolState.ConnectionResponseSent, - _, - _, - Some(resp), - _, // metaRetries: Int, - _, // metaLastFailure: Option[String] - ) => - ZIO.succeed(DidIdPair(resp.from, resp.to)) - case ConnectionRecord( - _, - _, - _, - _, - _, - Role.Invitee, - ProtocolState.ConnectionResponseReceived, - _, - _, - Some(resp), - _, // metaRetries: Int, - _, // metaLastFailure: Option[String] - ) => - ZIO.succeed(DidIdPair(resp.to, resp.from)) - case _ => - ZIO.fail(badRequestErrorResponse(Some("Invalid connection record state for operation"))) - } yield didIdPair - } + private[this] def getPairwiseDIDs(connectionId: String): ZIO[Any, ErrorResponse, DidIdPair] = { + for { + maybeConnection <- connectionService + .getConnectionRecord(UUID.fromString(connectionId)) + .mapError(HttpServiceError.DomainError[ConnectionServiceError].apply) + .mapError(_.toOAS) + connection <- ZIO + .fromOption(maybeConnection) + .mapError(_ => notFoundErrorResponse(Some("Connection not found"))) + connectionResponse <- ZIO + .fromOption(connection.connectionResponse) + .mapError(_ => notFoundErrorResponse(Some("ConnectionResponse not found in record"))) + didIdPair <- connection match + case ConnectionRecord( + _, + _, + _, + _, + _, + Role.Inviter, + ProtocolState.ConnectionResponseSent, + _, + _, + Some(resp), + _, // metaRetries: Int, + _, // metaLastFailure: Option[String] + ) => + ZIO.succeed(DidIdPair(resp.from, resp.to)) + case ConnectionRecord( + _, + _, + _, + _, + _, + Role.Invitee, + ProtocolState.ConnectionResponseReceived, + _, + _, + Some(resp), + _, // metaRetries: Int, + _, // metaLastFailure: Option[String] + ) => + ZIO.succeed(DidIdPair(resp.to, resp.from)) + case _ => + ZIO.fail(badRequestErrorResponse(Some("Invalid connection record state for operation"))) + } yield didIdPair + } } diff --git a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala index 424fc67b32..0e0e88649b 100644 --- a/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala +++ b/prism-agent/service/server/src/main/scala/io/iohk/atala/agent/server/jobs/BackgroundJobs.scala @@ -31,8 +31,6 @@ import io.iohk.atala.agent.walletapi.model._ import io.iohk.atala.agent.walletapi.model.error._ import io.iohk.atala.agent.walletapi.model.error.DIDSecretStorageError.KeyNotFoundError import io.iohk.atala.agent.walletapi.service.ManagedDIDService -import io.iohk.atala.agent.walletapi.sql.JdbcDIDSecretStorage -import io.iohk.atala.agent.walletapi.storage.DIDSecretStorage import io.iohk.atala.pollux.vc.jwt.ES256KSigner import io.iohk.atala.castor.core.model.did._ import java.security.KeyFactory @@ -55,6 +53,7 @@ import io.circe.parser._ import zio.prelude.AssociativeBothOps import zio.prelude.Validation import cats.syntax.all._ +import io.iohk.atala.castor.core.service.DIDService object BackgroundJobs { @@ -95,7 +94,7 @@ object BackgroundJobs { private[this] def performExchange( record: IssueCredentialRecord ): URIO[ - DidOps & DIDResolver & JwtDidResolver & HttpClient & CredentialService & ManagedDIDService & DIDSecretStorage, + DidOps & DIDResolver & JwtDidResolver & HttpClient & CredentialService & DIDService & ManagedDIDService, Unit ] = { import IssueCredentialRecord._ @@ -105,7 +104,7 @@ object BackgroundJobs { _ <- ZIO.logDebug(s"Running action with records => $record") _ <- record match { // Offer should be sent from Issuer to Holder - case IssueCredentialRecord(id, _, _, _, _, Role.Issuer, _, _, _, _, OfferPending, _, Some(offer), _, _, _) => + case IssueCredentialRecord(id, _, _, _, _, Role.Issuer, _, _, _, _, OfferPending, _, Some(offer), _, _, _, _) => for { _ <- ZIO.log(s"IssueCredentialRecord: OfferPending (START)") didCommAgent <- buildDIDCommAgent(offer.from) @@ -137,6 +136,7 @@ object BackgroundJobs { Some(request), _, _, + _ ) => for { didCommAgent <- buildDIDCommAgent(request.from) @@ -168,6 +168,7 @@ object BackgroundJobs { _, _, _, + _ ) => for { credentialService <- ZIO.service[CredentialService] @@ -192,14 +193,14 @@ object BackgroundJobs { _, Some(issue), _, + Some(issuingDID) ) => // Generate the JWT Credential and store it in DB as an attacment to IssueCredentialData // Set ProtocolState to CredentialGenerated // Set PublicationState to PublicationPending for { credentialService <- ZIO.service[CredentialService] - // issuer = credentialService.createIssuer - issuer <- createPrismDIDIssuer() + issuer <- createPrismDIDIssuer(issuingDID) w3Credential <- credentialService.createCredentialPayloadFromRecord( record, issuer, @@ -234,6 +235,7 @@ object BackgroundJobs { _, Some(issue), _, + _ ) => for { didCommAgent <- buildDIDCommAgent(issue.from) @@ -265,6 +267,7 @@ object BackgroundJobs { _, Some(issue), _, + _ ) => for { didCommAgent <- buildDIDCommAgent(issue.from) @@ -276,8 +279,8 @@ object BackgroundJobs { } } yield () - case IssueCredentialRecord(id, _, _, _, _, _, _, _, _, _, ProblemReportPending, _, _, _, _, _) => ??? - case IssueCredentialRecord(id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.unit + case IssueCredentialRecord(id, _, _, _, _, _, _, _, _, _, ProblemReportPending, _, _, _, _, _, _) => ??? + case IssueCredentialRecord(id, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _, _) => ZIO.unit } } yield () @@ -296,58 +299,54 @@ object BackgroundJobs { } // TODO: Improvements needed here: - // - A single PrismDID genrated at agent startup should be used. // - For now, we include the long form in the JWT credential to facilitate validation on client-side, but resolution should be used instead. - // - There should be a way to retrieve the 'default' PrismDID from ManagedDIDService (use of an alias in DB record?) - // - Simplify convertion of ECKeyPair to JDK security classes - // - ECPrivateKey should probably remain 'private' and signing operation occur in ManagedDIDService - private[this] def createPrismDIDIssuer(): ZIO[ManagedDIDService & DIDSecretStorage, Throwable, Issuer] = { + // - Improve consistency in error handling (ATL-3210) + private[this] def createPrismDIDIssuer( + issuingDID: PrismDID, + verificationRelationship: VerificationRelationship = VerificationRelationship.AssertionMethod, + allowUnpublishedIssuingDID: Boolean = false + ): ZIO[DIDService & ManagedDIDService, Throwable, Issuer] = { for { managedDIDService <- ZIO.service[ManagedDIDService] - longFormPrismDID <- managedDIDService.createAndStoreDID( - ManagedDIDTemplate( - Seq( - DIDPublicKeyTemplate("issuing", VerificationRelationship.Authentication) - ), - Nil - ) - ) - didSecretStorage <- ZIO.service[DIDSecretStorage] - maybeECKeyPair <- didSecretStorage.getKey(longFormPrismDID.asCanonical, "issuing") - _ <- ZIO.logInfo(s"ECKeyPair => $maybeECKeyPair") - maybeIssuer <- ZIO.succeed(maybeECKeyPair.map(ecKeyPair => { - val ba = ecKeyPair.privateKey.toPaddedByteArray(EllipticCurve.SECP256K1) - val keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider()) - val ecParameterSpec = ECNamedCurveTable.getParameterSpec("secp256k1") - val ecNamedCurveSpec = ECNamedCurveSpec( - ecParameterSpec.getName(), - ecParameterSpec.getCurve(), - ecParameterSpec.getG(), - ecParameterSpec.getN() + didService <- ZIO.service[DIDService] + didState <- managedDIDService + .getManagedDIDState(issuingDID.asCanonical) + .mapError(e => RuntimeException(s"Error occured while getting did from wallet: ${e.toString}")) + .someOrFail(RuntimeException(s"Issuer DID does not exist in the wallet: $issuingDID")) + .flatMap { + case s: ManagedDIDState.Published => ZIO.succeed(s) + case s if allowUnpublishedIssuingDID => ZIO.succeed(s) + case _ => ZIO.fail(RuntimeException(s"Issuer DID must be published: $issuingDID")) + } + longFormPrismDID = PrismDID.buildLongFormFromOperation(didState.createOperation) + // Automatically infer keyId to use by resolving DID and choose the corresponding VerificationRelationship + issuingKeyId <- didService + .resolveDID(issuingDID) + .mapError(e => RuntimeException(s"Error occured while resolving Issuing DID during VC creation: ${e.toString}")) + .someOrFail(RuntimeException(s"Issuing DID resolution result is not found")) + .map { case (_, didData) => didData.publicKeys.find(_.purpose == verificationRelationship).map(_.id) } + .someOrFail( + RuntimeException(s"Issuing DID doesn't have a key in ${verificationRelationship.name} to use: $issuingDID") ) - val ecPrivateKeySpec = ECPrivateKeySpec(java.math.BigInteger(1, ba), ecNamedCurveSpec) - val privateKey = keyFactory.generatePrivate(ecPrivateKeySpec) - val bcECPoint = ecParameterSpec - .getG() - .multiply(privateKey.asInstanceOf[org.bouncycastle.jce.interfaces.ECPrivateKey].getD()) - val ecPublicKeySpec = ECPublicKeySpec( - new ECPoint( - bcECPoint.normalize().getAffineXCoord().toBigInteger(), - bcECPoint.normalize().getAffineYCoord().toBigInteger() - ), - ecNamedCurveSpec + ecKeyPair <- managedDIDService + .javaKeyPairWithDID(issuingDID.asCanonical, issuingKeyId) + .mapError(e => RuntimeException(s"Error occurred while getting issuer key-pair: ${e.toString}")) + .someOrFail( + RuntimeException(s"Issuer key-pair does not exist in the wallet: ${issuingDID.toString}#$issuingKeyId") ) - val publicKey = keyFactory.generatePublic(ecPublicKeySpec) - Issuer(io.iohk.atala.pollux.vc.jwt.DID(longFormPrismDID.toString), ES256KSigner(privateKey), publicKey) - })) - issuer = maybeIssuer.get + (privateKey, publicKey) = ecKeyPair + issuer = Issuer( + io.iohk.atala.pollux.vc.jwt.DID(longFormPrismDID.toString), + ES256KSigner(privateKey), + publicKey + ) } yield issuer } private[this] def performPresentation( record: PresentationRecord ): URIO[ - DidOps & DIDResolver & JwtDidResolver & HttpClient & PresentationService & ManagedDIDService & DIDSecretStorage, + DidOps & DIDResolver & JwtDidResolver & HttpClient & PresentationService & DIDService & ManagedDIDService, Unit ] = { import io.iohk.atala.pollux.core.model.PresentationRecord.ProtocolState._ @@ -406,7 +405,22 @@ object BackgroundJobs { for { presentationService <- ZIO.service[PresentationService] - prover <- createPrismDIDIssuer() // TODO Prover Prism DID should be coming from DB and resolvable + // TODO: Do not create new DID for presentation (ATL-3244) + proverDID <- ZIO.serviceWithZIO[ManagedDIDService]( + _.createAndStoreDID( + ManagedDIDTemplate( + publicKeys = Seq( + DIDPublicKeyTemplate("auth-1", VerificationRelationship.Authentication) + ), + services = Nil + ) + ) + ) + prover <- createPrismDIDIssuer( + proverDID, + verificationRelationship = VerificationRelationship.Authentication, + allowUnpublishedIssuingDID = true + ) presentationPayload <- presentationService.createPresentationPayloadFromRecord( id, prover, @@ -514,6 +528,9 @@ object BackgroundJobs { JwtPresentation.validatePresentation(JWT(base64Decoded), options.domain, options.challenge) case _ => Validation.unit }) + // https://www.w3.org/TR/vc-data-model/#proofs-signatures-0 + // A proof is typically attached to a verifiable presentation for authentication purposes + // and to a verifiable credential as a method of assertion. result <- JwtPresentation.verify( JWT(base64Decoded), JwtPresentation.PresentationVerificationOptions( @@ -526,7 +543,7 @@ object BackgroundJobs { verifySignature = true, verifyDates = false, leeway = Duration.Zero, - maybeProofPurpose = Some(VerificationRelationship.Authentication) + maybeProofPurpose = Some(VerificationRelationship.AssertionMethod) ) ) ) diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/GetKeyError.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/GetKeyError.scala new file mode 100644 index 0000000000..486f845923 --- /dev/null +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/model/error/GetKeyError.scala @@ -0,0 +1,8 @@ +package io.iohk.atala.agent.walletapi.model.error + +sealed trait GetKeyError + +object GetKeyError { + final case class WalletStorageError(cause: Throwable) extends GetKeyError + final case class KeyConstructionError(cause: Throwable) extends GetKeyError +} diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDService.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDService.scala index d45cd8dff4..edc981c117 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDService.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/service/ManagedDIDService.scala @@ -49,6 +49,14 @@ import scala.collection.immutable.ArraySeq import io.iohk.atala.mercury.PeerDID import io.iohk.atala.mercury.model.DidId import io.iohk.atala.agent.walletapi.sql.JdbcDIDSecretStorage +import org.bouncycastle.jce.ECNamedCurveTable +import org.bouncycastle.jce.provider.BouncyCastleProvider + +import java.security.spec.ECPoint +import java.security.{KeyFactory, PrivateKey as JavaPrivateKey, PublicKey as JavaPublicKey} +import org.bouncycastle.jce.spec.ECNamedCurveSpec +import java.security.spec.ECPrivateKeySpec +import java.security.spec.ECPublicKeySpec /** A wrapper around Castor's DIDService providing key-management capability. Analogous to the secretAPI in * indy-wallet-sdk. @@ -94,9 +102,64 @@ final class ManagedDIDService private[walletapi] ( } yield () } + // FIXME + // Instead of returning the privateKey directly, it should provide more secure interface like + // {{{ def signWithDID(did, keyId, bytes): IO[?, Array[Byte]] }}}. + // For the time being, the purpose of this method is just to disallow SecretStorage to be + // used outside of this module. + def javaKeyPairWithDID( + did: CanonicalPrismDID, + keyId: String + ): IO[GetKeyError, Option[(JavaPrivateKey, JavaPublicKey)]] = { + secretStorage + .getKey(did, keyId) + .mapError(GetKeyError.WalletStorageError.apply) + .flatMap { maybeKeyPair => + maybeKeyPair.fold(ZIO.none) { ecKeyPair => + ZIO + .attempt { + // TODO: Simplify conversion of ECKeyPair to JDK security classes + val ba = ecKeyPair.privateKey.toPaddedByteArray(EllipticCurve.SECP256K1) + val keyFactory = KeyFactory.getInstance("EC", new BouncyCastleProvider()) + val ecParameterSpec = ECNamedCurveTable.getParameterSpec("secp256k1") + val ecNamedCurveSpec = ECNamedCurveSpec( + ecParameterSpec.getName(), + ecParameterSpec.getCurve(), + ecParameterSpec.getG(), + ecParameterSpec.getN() + ) + val ecPrivateKeySpec = ECPrivateKeySpec(java.math.BigInteger(1, ba), ecNamedCurveSpec) + val privateKey = keyFactory.generatePrivate(ecPrivateKeySpec) + val bcECPoint = ecParameterSpec + .getG() + .multiply(privateKey.asInstanceOf[org.bouncycastle.jce.interfaces.ECPrivateKey].getD()) + val ecPublicKeySpec = ECPublicKeySpec( + new ECPoint( + bcECPoint.normalize().getAffineXCoord().toBigInteger(), + bcECPoint.normalize().getAffineYCoord().toBigInteger() + ), + ecNamedCurveSpec + ) + val publicKey = keyFactory.generatePublic(ecPublicKeySpec) + (privateKey, publicKey) + } + .mapError(GetKeyError.KeyConstructionError.apply) + .asSome + } + } + } + + def getManagedDIDState(did: CanonicalPrismDID): IO[GetManagedDIDError, Option[ManagedDIDState]] = + for { + // state in wallet maybe stale, update it from DLT + _ <- computeNewDIDStateFromDLTAndPersist(did) + state <- nonSecretStorage.getManagedDIDState(did).mapError(GetManagedDIDError.WalletStorageError.apply) + } yield state + def listManagedDID: IO[GetManagedDIDError, Seq[ManagedDIDDetail]] = for { - _ <- syncManagedDIDState // state in wallet maybe stale, update it from DLT + // state in wallet maybe stale, update it from DLT + _ <- syncManagedDIDState dids <- nonSecretStorage.listManagedDID.mapError(GetManagedDIDError.WalletStorageError.apply) } yield dids.toSeq.map { case (did, state) => ManagedDIDDetail(did.asCanonical, state) } diff --git a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorage.scala b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorage.scala index 25f2f9f510..7395ac4697 100644 --- a/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorage.scala +++ b/prism-agent/service/wallet-api/src/main/scala/io/iohk/atala/agent/walletapi/storage/DIDSecretStorage.scala @@ -12,7 +12,7 @@ import io.iohk.atala.mercury.model.DidId import scala.collection.immutable.ArraySeq /** A simple single-user DID key storage */ -trait DIDSecretStorage { +private[walletapi] trait DIDSecretStorage { /** Returns a list of keys */ def listKeys(did: PrismDID): Task[Seq[(String, ArraySeq[Byte], ECKeyPair)]]