Skip to content

Commit

Permalink
feat(prism-agent): issue credential to Prism DID holder by Prism DID …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
patlo-iog authored Feb 15, 2023
1 parent 45bff75 commit 1c1a171
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 140 deletions.
27 changes: 24 additions & 3 deletions prism-agent/service/api/http/pollux/schemas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,7 @@ components:

# Issue Credential Protocol

CreateIssueCredentialRecordRequest:
description: A request to create a new "issue credential record"
IssueCredentialRecordBase:
required:
- subjectId
- claims
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
40 changes: 36 additions & 4 deletions prism-agent/service/issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -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' \
Expand All @@ -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,
Expand All @@ -37,15 +69,15 @@ curl -X 'POST' \
"lastname": "Wonderland",
"birthdate": "01/01/2000"
}
}' | jq
}' | jq
```

- **Holder** - Retrieving the list of issue records
```bash
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
Expand All @@ -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
```
```
2 changes: 1 addition & 1 deletion prism-agent/service/project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,6 @@ object Main extends ZIOAppDefault {
HttpModule.layers,
RepoModule.credentialSchemaServiceLayer,
AppModule.manageDIDServiceLayer,
JdbcDIDSecretStorage.layer,
RepoModule.agentTransactorLayer,
RepoModule.verificationPolicyServiceLayer
)
} yield app
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
//

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand Down Expand Up @@ -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

}

}
Expand Down
Loading

0 comments on commit 1c1a171

Please sign in to comment.