This endpoint resolves a DID. As input it takes the DID, plus DID resolution - options. The output is a DID - document - in one of the supported representations, plus metadata. The same endpoint can also be used to dereference a DID - URL. In this case, the output is a DID document, or part of a DID document, or some other resource identified - by the DID URL.
-See the DID Resolution - specification for additional details.
- operationId: resolve - tags: - - Universal Resolver - parameters: - - in: path - required: true - name: identifier - schema: - type: string - description: The DID to be resolved, or the DID URL to be dereferenced. - examples: - example1: - value: did:sov:builder:VbPQNHsvoLZdaNU7fTBeFx - description: A DID using the `sov` method. - example2: - value: did:ion:EiClkZMDxPKqC9c-umQfTkR8vvZ9JPhl_xLDI9Nfk38w5w - description: A DID using the `ion` method. - example3: - value: did:ebsi:z25ZZFS7FweHsm9MX2Qvc6gc - description: A DID using the `ebsi` method. - example4: - value: did:sov:builder:VbPQNHsvoLZdaNU7fTBeFx#key-1 - description: A DID URL with a fragment. - example5: - value: did:ebsi:z25ZZFS7FweHsm9MX2Qvc6gc#keys-1 - description: A DID URL with a fragment. - - in: header - required: false - name: Accept - schema: - type: string - description: The requested media type of the DID document representation or DID resolution result. See https://www.w3.org/TR/did-core/#representations and https://w3c-ccg.github.io/did-resolution/#did-resolution-result. - examples: - application/did+json: - value: application/json - description: Media type of a DID document (JSON representation). - application/did+ld+json: - value: application/did+ld+json - description: Media type of a DID document (JSON-LD representation). - application/ld+json;profile="https://w3id.org/did-resolution": - value: application/ld+json;profile="https://w3id.org/did-resolution" - description: Media type of a DID resolution result (JSON-LD representation). - responses: - '200': - description: successfully resolved! - content: - application/did+json: - schema: - type: object - description: The DID document (JSON representation). - example: { - "id": "did:sov:WRfXPg8dantKVubE3HX8pw", - "verificationMethod": [ - { - "id": "did:sov:WRfXPg8dantKVubE3HX8pw#key-1", - "type": "Ed25519VerificationKey2018", - "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" - } - ] - } - application/did+ld+json: - schema: - type: object - description: The DID document (JSON-LD representation). - example: { - "@context": "https://www.w3.org/ns/did/v1", - "id": "did:sov:WRfXPg8dantKVubE3HX8pw", - "verificationMethod": [ - { - "id": "did:sov:WRfXPg8dantKVubE3HX8pw#key-1", - "type": "Ed25519VerificationKey2018", - "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" - } - ] - } - application/ld+json;profile="https://w3id.org/did-resolution": - schema: - $ref: '#/components/schemas/UniversalResolver.ResolutionResult' - '400': - description: invalid input! - '410': - description: successfully resolved (deactivated)! - content: - application/did+json: - schema: - type: object - description: The deactivated DID document (JSON representation). - application/did+ld+json: - schema: - type: object - description: The deactivated DID document (JSON-LD representation). - application/ld+json;profile="https://w3id.org/did-resolution": - schema: - $ref: '#/components/schemas/UniversalResolver.ResolutionResult' - '500': - description: error! - /universal-resolver/properties: - get: - summary: Retrieve configuration properties. - description: -This endpoint returns a map of the configuration properties of the DID Resolver, including of its - drivers.
- operationId: universalResolverGetProperties - tags: - - Universal Resolver - responses: - '200': - description: Success. - content: - application/did+json: - schema: - type: object - description: A map of properties. - /universal-resolver/methods: - get: - summary: Retrieve supported DID methods. - description: -This endpoint returns a list of DID methods supported by the DID Resolver.
- operationId: universalResolverGetMethods - tags: - - Universal Resolver - responses: - '200': - description: Success. - content: - application/did+json: - schema: - type: array - items: - type: string - description: The list of supported DID methods. - example: ["btcr","sov","v1","key"] - /universal-resolver/testIdentifiers: - get: - summary: Retrieve a map of test identifiers. - description: -This endpoint returns a map of test identifiers that can be resolved by the DID Resolver, - grouped by DID method.
- operationId: universalResolverGetTestIdentifiers - tags: - - Universal Resolver - responses: - '200': - description: Success. - content: - application/did+json: - schema: - type: object - description: A map of test identifiers, grouped by DID method. - example: { "btcr": ["did:btcr:xz35-jznz-q9yu-ply", "did:btcr:xkrn-xz7q-qsye-28p"], "sov": ["did:sov:WRfXPg8dantKVubE3HX8pw"] } - /universal-registrar/create: - post: - summary: Create a DID. - description: -This endpoint creates a DID. As input it takes the DID, a DID document, plus optional DID registration - options and - secrets - needed to create a DID. The output is a state object that represents the current state of the DID creation - process.
-See the DID Registration - specification for additional details.
- operationId: create - tags: - - Universal Registrar - parameters: - - in: query - required: true - name: method - schema: - type: string - description: The requested DID method for the operation. - example: key - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.CreateRequest' - responses: - '200': - description: - The request was successful, but the DID may not be fully created yet, as indicated by the - "didState.state" and "jobId" output fields. Additional state information plus metadata are available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.CreateState' - '201': - description: - The DID has been successfully created, as indicated by the "didState.state" output field. - Additional state information (including the created DID) plus metadata are available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.CreateState' - '400': - description: - A problem with the input fields has occurred. Additional state information plus metadata may be - available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.CreateState' - '500': - description: - An internal error has occurred. Additional state information plus metadata may be - available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.CreateState' - /universal-registrar/update: - post: - summary: Update a DID. - description: -This endpoint updates a DID. As input it takes the existing DID, a DID document, plus optional DID registration - options and - secrets - needed to update a DID. The output is a state object that represents the current state of the DID update - process.
-See the DID Registration - specification for additional details.
- operationId: update - tags: - - Universal Registrar - parameters: - - in: query - required: true - name: method - schema: - type: string - description: The requested DID method for the operation. - example: btcr - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.UpdateRequest' - responses: - '200': - description: - The request was successful, and the DID may or may not be fully updated yet, as indicated by the - "didState.state" and "jobId" output fields. Additional state information plus metadata are available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.UpdateState' - '400': - description: - A problem with the input fields has occurred. Additional state information plus metadata may be - available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.UpdateState' - '500': - description: - An internal error has occurred. Additional state information plus metadata may be - available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.UpdateState' - /universal-registrar/deactivate: - post: - summary: Deactivate a DID. - description: -This endpoint deactivates a DID. As input it takes the existing DID, plus optional DID registration - options and - secrets - needed to deactivate a DID. The output is a state object that represents the current state of the DID deactivation - process.
-See the DID Registration - specification for additional details.
- operationId: deactivate - tags: - - Universal Registrar - parameters: - - in: query - required: true - name: method - schema: - type: string - description: The requested DID method for the operation. - example: btcr - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.DeactivateRequest' - responses: - '200': - description: - The request was successful, and the DID may or may not be fully deactivated yet, as indicated by the - "didState.state" and "jobId" output fields. Additional state information plus metadata are available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.DeactivateState' - '400': - description: - A problem with the input fields has occurred. Additional state information plus metadata may be - available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.DeactivateState' - '500': - description: - An internal error has occurred. Additional state information plus metadata may be - available in the response body. - content: - application/json: - schema: - $ref: '#/components/schemas/UniversalRegistrar.DeactivateState' - /universal-registrar/properties: - get: - summary: Retrieve configuration properties. - description: -This endpoint returns a map of the configuration properties of the DID Registrar, including of its - drivers.
- operationId: universalRegistrarGetProperties - tags: - - Universal Registrar - responses: - '200': - description: Success. - content: - application/did+json: - schema: - type: object - description: A map of properties. - /universal-registrar/methods: - get: - summary: Retrieve supported DID methods. - description: -This endpoint returns a list of DID methods supported by the DID Registrar.
- operationId: universalRegistrarGetMethods - tags: - - Universal Registrar - responses: - '200': - description: Success. - content: - application/did+json: - schema: - type: array - items: - type: string - description: The list of supported DID methods. - example: ["btcr","sov","v1","key"] - /wallet-service/keys: - get: - summary: Get key(s) - description: -This endpoint returns a list of public keys from the wallet service that match the input parameters.
-Note that there may be multiple keys for a given DID.
- operationId: getKeys - tags: - - Wallet Service - parameters: - - in: query - name: controller - schema: - type: string - format: uri - description: The controller of the key(s) to retrieve. - example: "did:sov:WRfXPg8dantKVubE3HX8pw" - - in: query - name: url - schema: - type: string - format: uri - description: The URL of the key(s) to retrieve. - example: "did:sov:WRfXPg8dantKVubE3HX8pw#keys-1" - - in: query - name: type - schema: - type: string - description: The type of the key(s) to retrieve. - example: "Ed25519" - - in: query - name: purpose - schema: - type: string - description: The purpose(s) of the key(s) to retrieve. - example: "authentication" - - in: query - name: limit - schema: - type: integer - format: int64 - description: The limit (total number) of keys to retrieve. - example: 1 - responses: - '200': - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/WalletService.Key' - description: The list of key(s). - example: [ - { - "id": "f25bed83-570c-45a4-b2fa-07a0df2a25d5", - "timestamp": 1633531154167, - "controller": "did:key:z6MkhES5w7ViszdQZBfKmzgFM8XVXy6DxVgBZzQCJdrcBuGz", - "url": "did:key:z6MkhES5w7ViszdQZBfKmzgFM8XVXy6DxVgBZzQCJdrcBuGz#z6MkhES5w7ViszdQZBfKmzgFM8XVXy6DxVgBZzQCJdrcBuGz", - "type": "Ed25519", - "purpose": "['authentication','assertionMethod','capabilityDelegation','capabilityInvocation']", - "key": { - "kty": "OKP", - "crv": "Ed25519", - "x": "KUnc6ztOeq1mR4UHxY2vmtJQZDk_VikOBTyPvrAu-c8" - }, - "keyMetadata": null - }, - { - "id": "618acb93-0c7d-4a0e-bb78-32745e436404", - "timestamp": 1633531154259, - "controller": "did:key:z6MkhES5w7ViszdQZBfKmzgFM8XVXy6DxVgBZzQCJdrcBuGz", - "url": "did:key:z6MkhES5w7ViszdQZBfKmzgFM8XVXy6DxVgBZzQCJdrcBuGz#z6LStgSrgnA1qWUdiEBcMEduv29jX1n8zmbgw1hcaewouhQV", - "type": "X25519", - "purpose": "['keyAgreement']", - "key": { - "kty": "OKP", - "crv": "X25519", - "x": "_Khr2RW7e-QAHqVx5RjDSshHomTjNDBCLeaJSQklgHI" - }, - "keyMetadata": null - } - ] - description: The request was successful, and the list of public key(s) has been retrieved. - '400': - description: A problem with the input fields has occurred. - '500': - description: An internal error has occurred. - post: - summary: Import key - description: -This endpoint imports a new private/public key in JWK into the wallet service.
-Note that when a DID is created using the Universal Registrar, - its keys are automatically imported into the wallet service.
- operationId: importKey - tags: - - Wallet Service - requestBody: - content: - application/json: - schema: - $ref: '#/components/schemas/WalletService.Key' - description: The new key to import. - responses: - '201': - description: The request was successful, and the new key has been imported. - '400': - description: A problem with the input fields has occurred. - '409': - description: This key already exists in the wallet service. - '500': - description: An internal error has occurred. - /wallet-service/keys/{id}: - get: - summary: Get key - description: -This endpoint returns a private/public key with a given internal identifier from the wallet service.
- operationId: getKey - tags: - - Wallet Service - parameters: - - in: path - name: id - required: true - schema: - type: string - format: uuid - description: The internal identifier of the key to retrieve. - example: "386fd0bf-6bf2-4063-a1b0-42927caf1886" - - in: query - name: exportPrivate - required: false - schema: - type: boolean - description: An optional flag to indicate whether private key data should be included in the response. - example: true - responses: - '200': - description: The request was successful, and the key has been retrieved. - content: - application/json: - schema: - $ref: '#/components/schemas/WalletService.Key' - '400': - description: A problem with the input fields has occurred. - '404': - description: No key was found with the given identifier. - '500': - description: An internal error has occurred. - delete: - summary: Delete key - description: -This endpoint deletes a private/public key from the wallet service.
-Note that deleting a key from the wallet service does not deactivate the corresponding DID. This should be - done before deleting keys.
-Also note that deleting a key may render a DID uncontrollable, unless the key was exported before. - operationId: deleteKey - tags: - - Wallet Service - parameters: - - in: path - name: id - required: true - schema: - type: string - format: uuid - description: The internal identifier of the key to delete. - example: "386fd0bf-6bf2-4063-a1b0-42927caf1886" - responses: - '204': - description: The request was successful, and the key has been deleted. - '400': - description: A problem with the input fields has occurred. - '404': - description: No key was found with the given identifier. - '500': - description: An internal error has occurred. - /wallet-service/keys/sign: - post: - summary: Sign with key - description: -
This endpoint signs an arbitrary unsigned payload using a private key from the wallet service.
- operationId: signWithKey - tags: - - Wallet Service - parameters: - - in: query - name: id - schema: - type: string - format: uuid - description: The internal identifier of the key to use for signing. - example: "386fd0bf-6bf2-4063-a1b0-42927caf1886" - - in: query - name: url - schema: - type: string - format: uri - description: The URL of the key to use for signing. - example: "did:sov:WRfXPg8dantKVubE3HX8pw#keys-1" - - in: query - name: algorithm - schema: - type: string - description: The cryptographic algorithm (JWA) to use for signing. - example: "EdDSA" - requestBody: - content: - application/octet-stream: - schema: - type: string - format: byte - description: The payload to sign. - responses: - '200': - content: - application/octet-stream: - schema: - type: string - format: byte - description: The request was successful, and the payload has been signed. - '400': - description: A problem with the input fields has occurred. - '404': - description: No key was found with the given identifier. - '500': - description: An internal error has occurred. - /wallet-service/keys/verify: - post: - summary: Verify with key - description: -This endpoint verifies an arbitrary signed payload using a public key from the wallet service.
- operationId: verifyWithKey - tags: - - Wallet Service - parameters: - - in: query - name: id - schema: - type: string - format: uuid - description: The internal identifier of the key to use for verifying. - example: "386fd0bf-6bf2-4063-a1b0-42927caf1886" - - in: query - name: url - schema: - type: string - format: uri - description: The URL of the key to use for verifying. - example: "did:sov:WRfXPg8dantKVubE3HX8pw#keys-1" - requestBody: - content: - application/octet-stream: - schema: - type: string - format: byte - description: The payload to verify. - responses: - '200': - content: - application/json: - schema: - type: object - description: The request was successful, and the payload has been verified. - '400': - description: A problem with the input fields has occurred. - '404': - description: No key was found with the given identifier. - '500': - description: An internal error has occurred. - /wallet-service/controllers: - get: - summary: Get controller(s) - description: -This endpoint returns a list of controllers from the wallet that match the input parameters.
- operationId: getControllers - parameters: - - in: query - name: type - schema: - type: string - description: The type of the controller(s) to retrieve. - example: "Ed25519" - - in: query - name: purpose - schema: - type: string - description: The purpose(s) of the controller(s) to retrieve. - example: "authentication" - - in: query - name: limit - schema: - type: integer - format: int64 - description: The limit (total number) of controllers to retrieve. - example: 1 - responses: - '200': - content: - application/json: - schema: - type: array - items: - type: string - description: The list of controller(s). - example: [ - "did:key:z6MkhES5w7ViszdQZBfKmzgFM8XVXy6DxVgBZzQCJdrcBuGz", - "did:key:z6MkiD3mSCJZFefR9GXY3bwa6cdxkLyUJ6a7HRFnU3YPNDp2", - "did:sov:danube:C7EGTrTaUruV98HPoEFP9n", - "did:v1:test:nym:z6MknvTVgWvRUHt5wcFQzRSbTReN95cKwo3EAbd1vEmUzUtt" - ] - description: The request was successful, and the list of controller(s) has been retrieved. - '400': - description: A problem with the input fields has occurred. - '404': - description: No controllers were found that match the input parameters. - '500': - description: An internal error has occurred. - /version-service/didrecords: - get: - summary: Get DID Record(s). - description: This endpoint gets matching DID Records. As input, it takes multiple optional parameters. The output is an array of DID Records if there are matches, empty otherwise.A DID Record represents a DID resolution at a specific point in time, with additional metadata attached to it. Results are sorted by versionTime in descending order. **limit** and **offset** parameters can be used in possible combinations unless stated otherwise. - operationId: getDidRecords - tags: - - Version Service - parameters: - - in: query - name: id - schema: - type: string - format: uuid - example: 8ff29f08-ede2-426d-b127-b95c3fa43192 - description: Unique id of the DID Record to get. When this parameter is set, no other parameter is allowed. - - in: query - name: did - schema: - type: string - format: did - example: did:ion:test:EiDnCmF6bjRF4GEB49PwGi3_AuOmXXBsSV4vor1fkNQc2w - description: A DID to get its DID Records. This parameter can be combined with **versionTime** to get DID Records up until that time or with **versionTimeFrom** and **versionTimeTo** to get DID Records in a given interval. - - in: query - name: method - schema: - type: string - example: jolo - description: A DID method to get its records. This parameter can be combined with **versionTime** to get DID Records of the given method up to (inclusive) the given time. - - in: query - name: versionId - schema: - type: string - example: 22 - description: Gets the DID Record of given **did** with given versionId. Usage only allowed with combination of the **did** field. No other parameters are allowed. - - in: query - name: versionTime - schema: - type: integer - format: int64 - example: 1610582180000 - description: Time in milliseconds. When used alone, this gets the records up to (inclusive) given time. - - in: query - name: versionTimeFrom - schema: - type: integer - format: int64 - description: Epoch time in milliseconds. **versionTime** must be null and **versionTimeTo** must be given to use time-range function. - example: 1610582180000 - - in: query - name: versionTimeTo - schema: - type: integer - format: int64 - example: 1610582180000 - description: Epoch time in milliseconds. **versionTime** must be null and **versionTimeFrom** must be given to use time-range function. - - in: query - name: offset - schema: - type: integer - description: The number of items to skip before starting to collect the result set. - - in: query - name: limit - schema: - type: integer - description: Maximum number of items to return. - responses: - '200': - description: DID record(s) successfully retrieved! - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/VersionService.DidRecord' - description: The DID record(s). - '400': - description: invalid input! - '404': - description: DID record(s) not found! - '500': - description: error! - /version-service/didrecords/findInIdentifier: - get: - summary: Find DID records(s) matching identifiers with given term - description: This endpoint searches the given term in DID Records' identifiers, i.e., DIDs. - operationId: findInIdentifier - tags: - - Version Service - parameters: - - in: query - name: term - required: true - schema: - type: string - example: abc - - in: query - name: offset - schema: - type: integer - description: The number of items to skip before starting to collect the result set. - - in: query - name: limit - schema: - type: integer - description: Maximum number of items to return. - responses: - '200': - description: DID Record(s) successfully retrieved! - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/VersionService.DidRecord' - description: The DID Record(s). - '400': - description: invalid input! - '404': - description: DID record(s) not found! - '500': - description: error! - /version-service/didrecords/methods: - get: - summary: Get a list of DID methods supported by the Version Service. - operationId: methods - tags: - - Version Service - responses: - '200': - description: DID Method list is successfully retrieved! - content: - application/json: - schema: - type: array - items: - type: string - '500': - description: error! - /version-service/didrecords/count: - get: - summary: Get a number of DID methods matching to given method. - operationId: countByMethod - parameters: - - in: query - name: method - schema: - type: string - responses: - '200': - description: Number of the DID Records for given method is retrieved! - content: - application/json: - schema: - type: integer - format: int64 - description: The matching did record count for the given method. - '500': - description: error! - /version-service/diddocuments: - get: - summary: Get DID document(s) - operationId: getDidDocuments - tags: - - Version Service - parameters: - - in: query - name: id - schema: - type: string - format: uuid - - in: query - name: did - schema: - type: string - format: did - - in: query - name: method - schema: - type: string - - in: query - name: offset - schema: - type: integer - description: The number of items to skip before starting to collect the result set. - - in: query - name: limit - schema: - type: integer - description: Maximum number of items to return. - responses: - '200': - description: DID document(s) successfully retrieved! - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/VersionService.DidDocument' - description: The DID document(s). - '400': - description: invalid input! - '404': - description: DID document(s) not found! - '500': - description: error! - /version-service/diddocuments/count: - get: - summary: Count DID document(s) matching to given path-value - operationId: countWithinDidDocuments - tags: - - Version Service - parameters: - - in: query - name: path - schema: - type: array - items: - type: string - - in: query - name: value - schema: - type: object - responses: - '200': - description: DID document(s) successfully retrieved! - content: - application/json: - schema: - type: integer - format: int64 - description: The matching did document count. - '400': - description: invalid input! - '404': - description: DID document(s) not found! - '500': - description: error! - /version-service/diddocuments/search: - get: - summary: Find DID document(s) matching to given path-value - operationId: findInDidDocumentContents - tags: - - Version Service - parameters: - - in: query - name: path - schema: - type: array - items: - type: string - - in: query - name: value - schema: - type: object - - in: query - name: offset - schema: - type: integer - description: The number of items to skip before starting to collect the result set. - - in: query - name: limit - schema: - type: integer - description: Maximum number of items to return. - responses: - '200': - description: DID document(s) successfully retrieved! - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/VersionService.DidDocument' - description: The DID document(s). - '400': - description: invalid input! - '404': - description: DID document(s) not found! - '500': - description: error! - /version-service/diddocuments/metadata/count: - get: - summary: Count DID document metadata matching to given path-value - operationId: countWithinDidDocumentMetadata - tags: - - Version Service - parameters: - - in: query - name: path - schema: - type: array - items: - type: string - - in: query - name: value - schema: - type: object - responses: - '200': - description: DID document(s) successfully retrieved! - content: - application/json: - schema: - type: integer - format: int64 - description: The matching did document metadata count. - '400': - description: invalid input! - '404': - description: DID document(s) not found! - '500': - description: error! - /version-service/diddocuments/metadata/search: - get: - summary: Find DID document metadata matching to given path-value - operationId: findInDidDocumentMetadata - tags: - - Version Service - parameters: - - in: query - name: path - schema: - type: array - items: - type: string - - in: query - name: value - schema: - type: object - - in: query - name: offset - schema: - type: integer - description: The number of items to skip before starting to collect the result set. - - in: query - name: limit - schema: - type: integer - description: Maximum number of items to return. - - in: query - name: sort - schema: - type: boolean - description: Sort descending according to record time, ignored in full-text search. - responses: - '200': - description: DID document(s) successfully retrieved! - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/VersionService.DidDocument' - description: The DID document(s). - '400': - description: invalid input! - '404': - description: DID document(s) not found! - '500': - description: error! -components: - schemas: - UniversalResolver.ResolutionResult: - description: The DID resolution result. - type: object - additionalProperties: false - properties: - didDocument: - type: object - example: { - "@context": "https://www.w3.org/ns/did/v1", - "id": "did:sov:WRfXPg8dantKVubE3HX8pw", - "verificationMethod": [ - { - "id": "did:sov:WRfXPg8dantKVubE3HX8pw#key-1", - "type": "Ed25519VerificationKey2018", - "publicKeyBase58": "H3C2AVvLMv6gmMNam3uVAjZpfkcJCwDwnZn6z3wXmqPV" - } - ] - } - didResolutionMetadata: - type: object - didDocumentMetadata: - type: object - UniversalRegistrar.CreateRequest: - description: Input fields for the create operation. - type: object - additionalProperties: false - properties: - jobId: - description: - This input field is used to keep track of an ongoing DID creation process. - See https://identity.foundation/did-registration/#jobid. - type: string - example: 6d85bcd0-2ea3-4288-ab00-15afadd8a156 - options: - description: - This input field contains an object with various options for the DID create operation, such as the network where the DID should be created. - See https://identity.foundation/did-registration/#options. - type: object - example: {"chain": "testnet"} - secret: - description: - This input field contains an object with DID controller keys and other secrets needed for performing the DID create operation. - See https://identity.foundation/did-registration/#secret. - type: object - example: {"seed": "72WGp7NgFR1Oqdi8zlt7jQQ434XR0cNQ"} - didDocument: - description: - This input field contains either a complete DID document, or an incremental change (diff) to a DID document. - See https://identity.foundation/did-registration/#diddocument. - type: object - UniversalRegistrar.UpdateRequest: - description: Input fields for the update operation. - type: object - additionalProperties: false - required: - - did - properties: - jobId: - description: - This input field is used to keep track of an ongoing DID update process. - See https://identity.foundation/did-registration/#jobid. - type: string - example: 6d85bcd0-2ea3-4288-ab00-15afadd8a156 - did: - description: - This input field indicates the DID that is the target of the DID update operation. - type: string - example: "did:sov:WRfXPg8dantKVubE3HX8pw" - options: - description: - This input field contains an object with various options for the DID update operation. - See https://identity.foundation/did-registration/#options. - type: object - secret: - description: - This input field contains an object with DID controller keys and other secrets needed for performing the DID update operation. - See https://identity.foundation/did-registration/#secret. - type: object - didDocumentOperation: - description: - This input field indicates which update operation should be applied to a DID’s associated DID document. - See https://identity.foundation/did-registration/#diddocumentoperation. - type: array - items: - type: string - didDocument: - description: - This input field contains either a complete DID document, or an incremental change (diff) to a DID document. - See https://identity.foundation/did-registration/#diddocument. - type: array - items: - type: object - UniversalRegistrar.DeactivateRequest: - description: Input fields for the deactivate operation. - type: object - additionalProperties: false - required: - - did - properties: - jobId: - description: - This input field is used to keep track of an ongoing DID deactivation process. - See https://identity.foundation/did-registration/#jobid. - type: string - example: 6d85bcd0-2ea3-4288-ab00-15afadd8a156 - did: - description: - This input field indicates the DID that is the target of the DID deactivation operation. - type: string - example: "did:sov:WRfXPg8dantKVubE3HX8pw" - options: - description: - This input field contains an object with various options for the DID deactivate operation. - See https://identity.foundation/did-registration/#options. - type: object - secret: - description: - This input field contains an object with DID controller keys and other secrets needed for performing the DID deactivate operation. - See https://identity.foundation/did-registration/#secret. - type: object - UniversalRegistrar.CreateState: - description: The state after a create operation. - type: object - additionalProperties: false - required: - - didState - properties: - jobId: - type: string - example: 6d85bcd0-2ea3-4288-ab00-15afadd8a156 - didState: - $ref: '#/components/schemas/UniversalRegistrar.DidState' - didRegistrationMetadata: - type: object - didDocumentMetadata: - type: object - UniversalRegistrar.UpdateState: - description: The state after an update operation. - type: object - additionalProperties: false - required: - - didState - properties: - jobId: - type: string - example: 6d85bcd0-2ea3-4288-ab00-15afadd8a156 - didState: - $ref: '#/components/schemas/UniversalRegistrar.DidState' - didRegistrationMetadata: - type: object - didDocumentMetadata: - type: object - UniversalRegistrar.DeactivateState: - description: The state after a deactivate operation. - type: object - additionalProperties: false - required: - - didState - properties: - jobId: - type: string - example: 6d85bcd0-2ea3-4288-ab00-15afadd8a156 - didState: - $ref: '#/components/schemas/UniversalRegistrar.DidState' - didRegistrationMetadata: - type: object - didDocumentMetadata: - type: object - UniversalRegistrar.DidState: - description: The current state of a DID. - type: object - additionalProperties: true - properties: - state: - type: string - example: "finished" - did: - type: string - example: "did:sov:WRfXPg8dantKVubE3HX8pw" - secret: - type: object - example: { - "verificationMethod": [{ - "id": "did:sov:WRfXPg8dantKVubE3HX8pw#key-1", - "type": "JsonWebKey2020", - "privateKeyJwk": { - "kty": "OKP", - "d": "NzJXR3A3TmdGUjFPcWRpOHpsdDdqUVE0MzRYUjBjTlE", - "crv": "Ed25519", - "x": "jpIKKU2b77lNXKTNW2NGvw1GUMjU6v_l_tLJAH5uYz0" - } - }] - } - didDocument: - type: object - WalletService.Key: - type: object - required: - - type - - key - properties: - id: - type: string - format: uuid - example: "f25bed83-570c-45a4-b2fa-07a0df2a25d5" - timestamp: - type: integer - format: int64 - example: 1633531154167 - controller: - type: string - format: uri - example: "did:key:z6MkhES5w7ViszdQZBfKmzgFM8XVXy6DxVgBZzQCJdrcBuGz" - url: - type: string - format: uri - example: "did:key:z6MkhES5w7ViszdQZBfKmzgFM8XVXy6DxVgBZzQCJdrcBuGz#z6MkhES5w7ViszdQZBfKmzgFM8XVXy6DxVgBZzQCJdrcBuGz" - type: - type: string - example: "Ed25519" - purpose: - type: string - example: "['authentication','assertionMethod','capabilityDelegation','capabilityInvocation']" - key: - type: object - example: { - "kty": "OKP", - "crv": "Ed25519", - "x": "KUnc6ztOeq1mR4UHxY2vmtJQZDk_VikOBTyPvrAu-c8" - } - keyMetadata: - type: object - example: { - } - VersionService.DidRecord: - type: object - required: - - did - - method - properties: - id: - type: string - format: uuid - example: 278dad34-e30b-4159-9ac0-ff5ccc0fdd81 - timestamp: - type: integer - format: int64 - example: 1624014333778 - description: Record's epoch time in milliseconds - did: - type: string - format: did - example: did:sov:WRfXPg8dantKVubE3HX8pw - method: - type: string - example: sov - description: DID Method name - didDocument: - $ref: '#/components/schemas/VersionService.DidDocument' - didVersion: - $ref: '#/components/schemas/VersionService.DidVersion' - didState: - $ref: '#/components/schemas/VersionService.DidState' - VersionService.DidDocument: - type: object - required: - - documentContent - properties: - didRecordId: - type: string - format: uuid - example: 8ff29f08-ede2-426d-b127-b95c3fa43192 - did: - type: string - format: did - example: did:sov:WRfXPg8dantKVubE3HX8pw - documentContent: - type: string - format: json - description: Must be in form of json string. - documentMetadata: - type: string - format: json - description: Must be in form of json string. - VersionService.DidVersion: - type: object - properties: - didRecordId: - type: string - format: uuid - example: 8ff29f08-ede2-426d-b127-b95c3fa43192 - did: - type: string - format: did - example: did:sov:WRfXPg8dantKVubE3HX8pw - versionId: - type: string - example: 18 - versionTime: - type: integer - format: int64 - example: 1624014333778 - description: Version's epoch time in milliseconds - versionMetadata: - type: string - format: json - description: Must be in form of json string. - VersionService.DidState: - type: object - required: - - state - properties: - didRecordId: - type: string - format: uuid - example: 8ff29f08-ede2-426d-b127-b95c3fa43192 - did: - type: string - format: did - example: did:sov:WRfXPg8dantKVubE3HX8pw - state: - type: string - example: pending - stateTime: - type: integer - format: int64 - example: 1624014333778 - stateMetadata: - type: string - format: json - description: Must be in form of json string. diff --git a/examples/mt-keycloak-vault/compose.yaml b/examples/mt-keycloak-vault/compose.yaml new file mode 100644 index 0000000000..daf6939b36 --- /dev/null +++ b/examples/mt-keycloak-vault/compose.yaml @@ -0,0 +1,167 @@ +configs: + caddyfile_default: + content: |- + :8080 { + handle_path /didcomm* { + reverse_proxy agent-default:8090 + } + handle_path /prism-agent* { + reverse_proxy agent-default:8085 + } + handle_path /keycloak* { + reverse_proxy keycloak-default:8080 + } + handle_path /vault* { + reverse_proxy vault-default:8200 + } + } +services: + agent-default: + depends_on: + keycloak-init-default: + condition: service_completed_successfully + node: + condition: service_started + environment: + ADMIN_TOKEN: admin + AGENT_DB_HOST: db-default + AGENT_DB_NAME: agent + AGENT_DB_PASSWORD: postgres + AGENT_DB_PORT: '5432' + AGENT_DB_USER: postgres + API_KEY_ENABLED: 'false' + CONNECT_DB_HOST: db-default + CONNECT_DB_NAME: connect + CONNECT_DB_PASSWORD: postgres + CONNECT_DB_PORT: '5432' + CONNECT_DB_USER: postgres + DIDCOMM_SERVICE_URL: http://caddy-default:8080/didcomm + KEYCLOAK_CLIENT_ID: agent + KEYCLOAK_CLIENT_SECRET: agent-secret + KEYCLOAK_ENABLED: 'true' + KEYCLOAK_REALM: identus + KEYCLOAK_URL: http://keycloak-default:8080 + POLLUX_DB_HOST: db-default + POLLUX_DB_NAME: pollux + POLLUX_DB_PASSWORD: postgres + POLLUX_DB_PORT: '5432' + POLLUX_DB_USER: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-default:8080/prism-agent + PRISM_NODE_HOST: node + PRISM_NODE_PORT: '50053' + REST_SERVICE_URL: http://caddy-default:8080/prism-agent + SECRET_STORAGE_BACKEND: vault + VAULT_ADDR: http://vault-default:8200 + VAULT_TOKEN: admin + image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + restart: always + caddy-default: + configs: + - source: caddyfile_default + target: /etc/caddy/Caddyfile + image: caddy:2.7.6-alpine + ports: + - 8080:8080 + restart: always + db-default: + environment: + POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_default:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + keycloak-default: + command: + - start-dev + - --features=preview + - --health-enabled=true + - --hostname-url=http://localhost:8080/keycloak + - --hostname-admin-url=http://localhost:8080/keycloak + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + image: quay.io/keycloak/keycloak:23.0.7 + restart: always + keycloak-init-default: + command: + - --glob + - /hurl/*.hurl + - --test + environment: + HURL_KEYCLOAK_ADMIN_PASSWORD: admin + HURL_KEYCLOAK_ADMIN_USER: admin + HURL_KEYCLOAK_BASE_URL: http://keycloak-default:8080 + HURL_KEYCLOAK_CLIENT_ID: agent + HURL_KEYCLOAK_CLIENT_SECRET: agent-secret + HURL_KEYCLOAK_REALM: identus + image: ghcr.io/orange-opensource/hurl:4.2.0 + volumes: + - ../.shared/hurl/simple_realm:/hurl + node: + depends_on: + node-db: + condition: service_healthy + environment: + NODE_PSQL_DATABASE: node_db + NODE_PSQL_HOST: node-db:5432 + NODE_PSQL_PASSWORD: postgres + NODE_PSQL_USERNAME: postgres + image: ghcr.io/input-output-hk/prism-node:2.4.0 + restart: always + node-db: + environment: + POSTGRES_MULTIPLE_DATABASES: node_db + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_node:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + vault-default: + cap_add: + - IPC_LOCK + environment: + VAULT_ADDR: http://0.0.0.0:8200 + VAULT_DEV_ROOT_TOKEN_ID: admin + healthcheck: + interval: 10s + retries: '5' + test: + - CMD + - vault + - status + timeout: 5s + image: hashicorp/vault:1.15.6 + ports: + - 8200:8200 +volumes: + pg_data_default: {} + pg_data_node: {} diff --git a/examples/mt-keycloak-vault/tests/01_create_users.hurl b/examples/mt-keycloak-vault/tests/01_create_users.hurl new file mode 100644 index 0000000000..9a412b9668 --- /dev/null +++ b/examples/mt-keycloak-vault/tests/01_create_users.hurl @@ -0,0 +1,112 @@ +############################## +# Create users +############################## +# Admin login +POST {{ keycloak_base_url }}/realms/master/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: admin-cli +username: {{ keycloak_admin_user }} +password: {{ keycloak_admin_password }} +HTTP 200 +[Captures] +admin_access_token: jsonpath "$.access_token" + +# Create Issuer +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users +Authorization: Bearer {{ admin_access_token }} +{ + "username": "{{ issuer_username }}", + "firstName": "Alice", + "lastName": "Wonderland", + "enabled": true, + "email": "alice@atalaprism.io", + "credentials": [{"value": "{{ issuer_password }}", "temporary": false}] +} +HTTP 201 + +# Create Holder +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users +Authorization: Bearer {{ admin_access_token }} +{ + "username": "{{ holder_username }}", + "firstName": "SpongeBob", + "lastName": "SquarePants", + "enabled": true, + "email": "bob@atalaprism.io", + "credentials": [{"value": "{{ holder_password }}", "temporary": false}] +} +HTTP 201 + +# Create Verifier +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users +Authorization: Bearer {{ admin_access_token }} +{ + "username": "{{ verifier_username }}", + "firstName": "John", + "lastName": "Wick", + "enabled": true, + "email": "john@atalaprism.io", + "credentials": [{"value": "{{ verifier_password }}", "temporary": false}] +} +HTTP 201 + +############################## +# Create Wallets +############################## +# Issuer login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ issuer_username }} +password: {{ issuer_password }} +HTTP 200 +[Captures] +issuer_access_token: jsonpath "$.access_token" + +# Create Issuer wallet +POST {{ agent_url }}/prism-agent/wallets +Authorization: Bearer {{ issuer_access_token }} +{ + "name": "issuer-wallet" +} +HTTP 201 + +# Holder login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ holder_username }} +password: {{ holder_password }} +HTTP 200 +[Captures] +holder_access_token: jsonpath "$.access_token" + +# Create Holder wallet +POST {{ agent_url }}/prism-agent/wallets +Authorization: Bearer {{ holder_access_token }} +{ + "name": "holder-wallet" +} +HTTP 201 + +# Verifier login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ verifier_username }} +password: {{ verifier_password }} +HTTP 200 +[Captures] +verifier_access_token: jsonpath "$.access_token" + +# Create Verifier wallet +POST {{ agent_url }}/prism-agent/wallets +Authorization: Bearer {{ verifier_access_token }} +{ + "name": "verifier-wallet" +} +HTTP 201 diff --git a/examples/mt-keycloak-vault/tests/02_jwt_flow.hurl b/examples/mt-keycloak-vault/tests/02_jwt_flow.hurl new file mode 100644 index 0000000000..c1ceb033b4 --- /dev/null +++ b/examples/mt-keycloak-vault/tests/02_jwt_flow.hurl @@ -0,0 +1,282 @@ +############################## +# Login +############################## +# Issuer login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ issuer_username }} +password: {{ issuer_password }} +HTTP 200 +[Captures] +issuer_access_token: jsonpath "$.access_token" + +# Holder login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ holder_username }} +password: {{ holder_password }} +HTTP 200 +[Captures] +holder_access_token: jsonpath "$.access_token" + +# Verfier login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ verifier_username }} +password: {{ verifier_password }} +HTTP 200 +[Captures] +verifier_access_token: jsonpath "$.access_token" + + +############################## +# Prerequisites +############################## +# Issuer create DID +POST {{ agent_url }}/prism-agent/did-registrar/dids +Authorization: Bearer {{ issuer_access_token }} +{ + "documentTemplate": { + "publicKeys": [ + { + "id": "iss-key", + "purpose": "assertionMethod" + } + ], + "services": [] + } +} +HTTP 201 +[Captures] +issuer_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" + +# Holder create DID +POST {{ agent_url }}/prism-agent/did-registrar/dids +Authorization: Bearer {{ holder_access_token }} +{ + "documentTemplate": { + "publicKeys": [ + { + "id": "auth-key", + "purpose": "authentication" + } + ], + "services": [] + } +} +HTTP 201 +[Captures] +holder_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" + +############################## +# Issuance Connection +############################## +# Inviter create connection +POST {{ agent_url }}/prism-agent/connections +Authorization: Bearer {{ issuer_access_token }} +{ + "label": "My Connection" +} +HTTP 201 +[Captures] +raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" +issuer_connection_id: jsonpath "$.connectionId" + +# Invitee accept connection +POST {{ agent_url }}/prism-agent/connection-invitations +Authorization: Bearer {{ holder_access_token }} +{ + "invitation": "{{ raw_invitation }}" +} +HTTP 200 +[Captures] +holder_connection_id: jsonpath "$.connectionId" + +# Wait for inviter connection status +GET {{ agent_url }}/prism-agent/connections/{{ issuer_connection_id }} +Authorization: Bearer {{ issuer_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseSent" + +# Wait for invitee connection status +GET {{ agent_url }}/prism-agent/connections/{{ holder_connection_id }} +Authorization: Bearer {{ holder_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseReceived" + +############################## +# Issuance +############################## +# Issuer create credential offer +POST {{ agent_url }}/prism-agent/issue-credentials/credential-offers +Authorization: Bearer {{ issuer_access_token }} +{ + "claims": { + "emailAddress": "alice@wonderland.com", + "givenName": "Alice", + "familyName": "Wonderland" + }, + "credentialFormat": "JWT", + "issuingDID": "{{ issuer_did }}", + "connectionId": "{{ issuer_connection_id }}" +} +HTTP 201 +[Captures] +issuer_cred_record_id: jsonpath "$.recordId" +didcomm_issuing_thid: jsonpath "$.thid" + +# Holder wait for OfferReceived state +GET {{ agent_url }}/prism-agent/issue-credentials/records +Authorization: Bearer {{ holder_access_token }} +[QueryStringParams] +thid: {{ didcomm_issuing_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].protocolState" == "OfferReceived" +[Captures] +holder_cred_record_id: jsonpath "$.contents[0].recordId" + +# Holder accept a credential-offer +POST {{ agent_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }}/accept-offer +Authorization: Bearer {{ holder_access_token }} +{ + "subjectId": "{{ holder_did }}" +} +HTTP 200 + +# Holder wait for CredentialReceived state +GET {{ agent_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }} +Authorization: Bearer {{ holder_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.protocolState" == "CredentialReceived" + +# Issuer wait for CredentialSent state +GET {{ agent_url }}/prism-agent/issue-credentials/records/{{ issuer_cred_record_id }} +Authorization: Bearer {{ issuer_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.protocolState" == "CredentialSent" + +############################## +# Presentation Connection +############################## +# Inviter create connection +POST {{ agent_url }}/prism-agent/connections +Authorization: Bearer {{ verifier_access_token }} +{ + "label": "My Connection" +} +HTTP 201 +[Captures] +raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" +verifier_connection_id: jsonpath "$.connectionId" + +# Invitee accept connection +POST {{ agent_url }}/prism-agent/connection-invitations +Authorization: Bearer {{ holder_access_token }} +{ + "invitation": "{{ raw_invitation }}" +} +HTTP 200 +[Captures] +holder_connection_id: jsonpath "$.connectionId" + +# Wait for inviter connection status +GET {{ agent_url }}/prism-agent/connections/{{ verifier_connection_id }} +Authorization: Bearer {{ verifier_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseSent" + +# Wait for invitee connection status +GET {{ agent_url }}/prism-agent/connections/{{ holder_connection_id }} +Authorization: Bearer {{ holder_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseReceived" + +############################## +# Presentation +############################## +# Verifier create presentation request +POST {{ agent_url }}/prism-agent/present-proof/presentations +Authorization: Bearer {{ verifier_access_token }} +{ + "connectionId": "{{ verifier_connection_id }}", + "proofs":[], + "options": { + "challenge": "11c91493-01b3-4c4d-ac36-b336bab5bddf", + "domain": "https://prism-verifier.com" + } +} +HTTP 201 +[Captures] +verifier_presentation_id: jsonpath "$.presentationId" +didcomm_presentation_thid: jsonpath "$.thid" + +# Holder wait for RequestReceived state +GET {{ agent_url }}/prism-agent/present-proof/presentations +Authorization: Bearer {{ holder_access_token }} +[QueryStringParams] +thid: {{ didcomm_presentation_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].status" == "RequestReceived" +[Captures] +holder_presentation_id: jsonpath "$.contents[0].presentationId" + +# Holder accept presentation request +PATCH {{ agent_url }}/prism-agent/present-proof/presentations/{{ holder_presentation_id }} +Authorization: Bearer {{ holder_access_token }} +{ + "action": "request-accept", + "proofId": ["{{ holder_cred_record_id }}"] +} +HTTP 200 + +# Holder wait for PresentationSent state +GET {{ agent_url }}/prism-agent/present-proof/presentations +Authorization: Bearer {{ holder_access_token }} +[QueryStringParams] +thid: {{ didcomm_presentation_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].status" == "PresentationSent" + +# Verfiier wait for PresentationVerified state +GET {{ agent_url }}/prism-agent/present-proof/presentations +Authorization: Bearer {{ verifier_access_token }} +[QueryStringParams] +thid: {{ didcomm_presentation_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].status" == "PresentationVerified" diff --git a/examples/mt-keycloak-vault/tests/local b/examples/mt-keycloak-vault/tests/local new file mode 100644 index 0000000000..a376aaf58b --- /dev/null +++ b/examples/mt-keycloak-vault/tests/local @@ -0,0 +1,15 @@ +keycloak_admin_password=admin +keycloak_admin_user=admin +keycloak_base_url=http://localhost:8080/keycloak +keycloak_realm=identus + +agent_url=http://localhost:8080 + +issuer_password=1234 +issuer_username=alice + +holder_password=1234 +holder_username=bob + +verifier_password=1234 +verifier_username=john diff --git a/examples/mt-keycloak/compose.yaml b/examples/mt-keycloak/compose.yaml new file mode 100644 index 0000000000..3ebc37d1ad --- /dev/null +++ b/examples/mt-keycloak/compose.yaml @@ -0,0 +1,148 @@ +configs: + caddyfile_default: + content: |- + :8080 { + handle_path /didcomm* { + reverse_proxy agent-default:8090 + } + handle_path /prism-agent* { + reverse_proxy agent-default:8085 + } + handle_path /keycloak* { + reverse_proxy keycloak-default:8080 + } + handle_path /vault* { + reverse_proxy vault-default:8200 + } + } +services: + agent-default: + depends_on: + keycloak-init-default: + condition: service_completed_successfully + node: + condition: service_started + environment: + ADMIN_TOKEN: admin + AGENT_DB_HOST: db-default + AGENT_DB_NAME: agent + AGENT_DB_PASSWORD: postgres + AGENT_DB_PORT: '5432' + AGENT_DB_USER: postgres + API_KEY_ENABLED: 'false' + CONNECT_DB_HOST: db-default + CONNECT_DB_NAME: connect + CONNECT_DB_PASSWORD: postgres + CONNECT_DB_PORT: '5432' + CONNECT_DB_USER: postgres + DIDCOMM_SERVICE_URL: http://caddy-default:8080/didcomm + KEYCLOAK_CLIENT_ID: agent + KEYCLOAK_CLIENT_SECRET: agent-secret + KEYCLOAK_ENABLED: 'true' + KEYCLOAK_REALM: identus + KEYCLOAK_URL: http://keycloak-default:8080 + POLLUX_DB_HOST: db-default + POLLUX_DB_NAME: pollux + POLLUX_DB_PASSWORD: postgres + POLLUX_DB_PORT: '5432' + POLLUX_DB_USER: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-default:8080/prism-agent + PRISM_NODE_HOST: node + PRISM_NODE_PORT: '50053' + REST_SERVICE_URL: http://caddy-default:8080/prism-agent + SECRET_STORAGE_BACKEND: postgres + image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + restart: always + caddy-default: + configs: + - source: caddyfile_default + target: /etc/caddy/Caddyfile + image: caddy:2.7.6-alpine + ports: + - 8080:8080 + restart: always + db-default: + environment: + POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_default:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + keycloak-default: + command: + - start-dev + - --features=preview + - --health-enabled=true + - --hostname-url=http://localhost:8080/keycloak + - --hostname-admin-url=http://localhost:8080/keycloak + environment: + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + image: quay.io/keycloak/keycloak:23.0.7 + restart: always + keycloak-init-default: + command: + - --glob + - /hurl/*.hurl + - --test + environment: + HURL_KEYCLOAK_ADMIN_PASSWORD: admin + HURL_KEYCLOAK_ADMIN_USER: admin + HURL_KEYCLOAK_BASE_URL: http://keycloak-default:8080 + HURL_KEYCLOAK_CLIENT_ID: agent + HURL_KEYCLOAK_CLIENT_SECRET: agent-secret + HURL_KEYCLOAK_REALM: identus + image: ghcr.io/orange-opensource/hurl:4.2.0 + volumes: + - ../.shared/hurl/simple_realm:/hurl + node: + depends_on: + node-db: + condition: service_healthy + environment: + NODE_PSQL_DATABASE: node_db + NODE_PSQL_HOST: node-db:5432 + NODE_PSQL_PASSWORD: postgres + NODE_PSQL_USERNAME: postgres + image: ghcr.io/input-output-hk/prism-node:2.4.0 + restart: always + node-db: + environment: + POSTGRES_MULTIPLE_DATABASES: node_db + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_node:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql +volumes: + pg_data_default: {} + pg_data_node: {} diff --git a/examples/mt-keycloak/tests/01_create_users.hurl b/examples/mt-keycloak/tests/01_create_users.hurl new file mode 100644 index 0000000000..9a412b9668 --- /dev/null +++ b/examples/mt-keycloak/tests/01_create_users.hurl @@ -0,0 +1,112 @@ +############################## +# Create users +############################## +# Admin login +POST {{ keycloak_base_url }}/realms/master/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: admin-cli +username: {{ keycloak_admin_user }} +password: {{ keycloak_admin_password }} +HTTP 200 +[Captures] +admin_access_token: jsonpath "$.access_token" + +# Create Issuer +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users +Authorization: Bearer {{ admin_access_token }} +{ + "username": "{{ issuer_username }}", + "firstName": "Alice", + "lastName": "Wonderland", + "enabled": true, + "email": "alice@atalaprism.io", + "credentials": [{"value": "{{ issuer_password }}", "temporary": false}] +} +HTTP 201 + +# Create Holder +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users +Authorization: Bearer {{ admin_access_token }} +{ + "username": "{{ holder_username }}", + "firstName": "SpongeBob", + "lastName": "SquarePants", + "enabled": true, + "email": "bob@atalaprism.io", + "credentials": [{"value": "{{ holder_password }}", "temporary": false}] +} +HTTP 201 + +# Create Verifier +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users +Authorization: Bearer {{ admin_access_token }} +{ + "username": "{{ verifier_username }}", + "firstName": "John", + "lastName": "Wick", + "enabled": true, + "email": "john@atalaprism.io", + "credentials": [{"value": "{{ verifier_password }}", "temporary": false}] +} +HTTP 201 + +############################## +# Create Wallets +############################## +# Issuer login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ issuer_username }} +password: {{ issuer_password }} +HTTP 200 +[Captures] +issuer_access_token: jsonpath "$.access_token" + +# Create Issuer wallet +POST {{ agent_url }}/prism-agent/wallets +Authorization: Bearer {{ issuer_access_token }} +{ + "name": "issuer-wallet" +} +HTTP 201 + +# Holder login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ holder_username }} +password: {{ holder_password }} +HTTP 200 +[Captures] +holder_access_token: jsonpath "$.access_token" + +# Create Holder wallet +POST {{ agent_url }}/prism-agent/wallets +Authorization: Bearer {{ holder_access_token }} +{ + "name": "holder-wallet" +} +HTTP 201 + +# Verifier login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ verifier_username }} +password: {{ verifier_password }} +HTTP 200 +[Captures] +verifier_access_token: jsonpath "$.access_token" + +# Create Verifier wallet +POST {{ agent_url }}/prism-agent/wallets +Authorization: Bearer {{ verifier_access_token }} +{ + "name": "verifier-wallet" +} +HTTP 201 diff --git a/examples/mt-keycloak/tests/02_jwt_flow.hurl b/examples/mt-keycloak/tests/02_jwt_flow.hurl new file mode 100644 index 0000000000..2169d2f777 --- /dev/null +++ b/examples/mt-keycloak/tests/02_jwt_flow.hurl @@ -0,0 +1,296 @@ +############################## +# Login +############################## +# Issuer login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ issuer_username }} +password: {{ issuer_password }} +HTTP 200 +[Captures] +issuer_access_token: jsonpath "$.access_token" + +# Holder login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ holder_username }} +password: {{ holder_password }} +HTTP 200 +[Captures] +holder_access_token: jsonpath "$.access_token" + +# Verfier login +POST {{ keycloak_base_url }}/realms/{{ keycloak_realm }}/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: web-ui +username: {{ verifier_username }} +password: {{ verifier_password }} +HTTP 200 +[Captures] +verifier_access_token: jsonpath "$.access_token" + + +############################## +# Prerequisites +############################## +# Issuer create DID +POST {{ agent_url }}/prism-agent/did-registrar/dids +Authorization: Bearer {{ issuer_access_token }} +{ + "documentTemplate": { + "publicKeys": [ + { + "id": "iss-key", + "purpose": "assertionMethod" + } + ], + "services": [] + } +} +HTTP 201 +[Captures] +issuer_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" + +# Issuer publish DID +POST {{ agent_url }}/prism-agent/did-registrar/dids/{{ issuer_did }}/publications +Authorization: Bearer {{ issuer_access_token }} +HTTP 202 + +# Issuer wait for DID to be published +GET {{ agent_url }}/prism-agent/did-registrar/dids/{{ issuer_did }} +Authorization: Bearer {{ issuer_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.status" == "PUBLISHED" + +# Holder create DID +POST {{ agent_url }}/prism-agent/did-registrar/dids +Authorization: Bearer {{ holder_access_token }} +{ + "documentTemplate": { + "publicKeys": [ + { + "id": "auth-key", + "purpose": "authentication" + } + ], + "services": [] + } +} +HTTP 201 +[Captures] +holder_did: jsonpath "$.longFormDid" + +############################## +# Issuance Connection +############################## +# Inviter create connection +POST {{ agent_url }}/prism-agent/connections +Authorization: Bearer {{ issuer_access_token }} +{ + "label": "My Connection" +} +HTTP 201 +[Captures] +raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" +issuer_connection_id: jsonpath "$.connectionId" + +# Invitee accept connection +POST {{ agent_url }}/prism-agent/connection-invitations +Authorization: Bearer {{ holder_access_token }} +{ + "invitation": "{{ raw_invitation }}" +} +HTTP 200 +[Captures] +holder_connection_id: jsonpath "$.connectionId" + +# Wait for inviter connection status +GET {{ agent_url }}/prism-agent/connections/{{ issuer_connection_id }} +Authorization: Bearer {{ issuer_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseSent" + +# Wait for invitee connection status +GET {{ agent_url }}/prism-agent/connections/{{ holder_connection_id }} +Authorization: Bearer {{ holder_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseReceived" + +############################## +# Issuance +############################## +# Issuer create credential offer +POST {{ agent_url }}/prism-agent/issue-credentials/credential-offers +Authorization: Bearer {{ issuer_access_token }} +{ + "claims": { + "emailAddress": "alice@wonderland.com", + "givenName": "Alice", + "familyName": "Wonderland" + }, + "credentialFormat": "JWT", + "issuingDID": "{{ issuer_did }}", + "connectionId": "{{ issuer_connection_id }}" +} +HTTP 201 +[Captures] +issuer_cred_record_id: jsonpath "$.recordId" +didcomm_issuing_thid: jsonpath "$.thid" + +# Holder wait for OfferReceived state +GET {{ agent_url }}/prism-agent/issue-credentials/records +Authorization: Bearer {{ holder_access_token }} +[QueryStringParams] +thid: {{ didcomm_issuing_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].protocolState" == "OfferReceived" +[Captures] +holder_cred_record_id: jsonpath "$.contents[0].recordId" + +# Holder accept a credential-offer +POST {{ agent_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }}/accept-offer +Authorization: Bearer {{ holder_access_token }} +{ + "subjectId": "{{ holder_did }}" +} +HTTP 200 + +# Holder wait for CredentialReceived state +GET {{ agent_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }} +Authorization: Bearer {{ holder_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.protocolState" == "CredentialReceived" + +# Issuer wait for CredentialSent state +GET {{ agent_url }}/prism-agent/issue-credentials/records/{{ issuer_cred_record_id }} +Authorization: Bearer {{ issuer_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.protocolState" == "CredentialSent" + +############################## +# Presentation Connection +############################## +# Inviter create connection +POST {{ agent_url }}/prism-agent/connections +Authorization: Bearer {{ verifier_access_token }} +{ + "label": "My Connection" +} +HTTP 201 +[Captures] +raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" +verifier_connection_id: jsonpath "$.connectionId" + +# Invitee accept connection +POST {{ agent_url }}/prism-agent/connection-invitations +Authorization: Bearer {{ holder_access_token }} +{ + "invitation": "{{ raw_invitation }}" +} +HTTP 200 +[Captures] +holder_connection_id: jsonpath "$.connectionId" + +# Wait for inviter connection status +GET {{ agent_url }}/prism-agent/connections/{{ verifier_connection_id }} +Authorization: Bearer {{ verifier_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseSent" + +# Wait for invitee connection status +GET {{ agent_url }}/prism-agent/connections/{{ holder_connection_id }} +Authorization: Bearer {{ holder_access_token }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseReceived" + +############################## +# Presentation +############################## +# Verifier create presentation request +POST {{ agent_url }}/prism-agent/present-proof/presentations +Authorization: Bearer {{ verifier_access_token }} +{ + "connectionId": "{{ verifier_connection_id }}", + "proofs":[], + "options": { + "challenge": "11c91493-01b3-4c4d-ac36-b336bab5bddf", + "domain": "https://prism-verifier.com" + } +} +HTTP 201 +[Captures] +verifier_presentation_id: jsonpath "$.presentationId" +didcomm_presentation_thid: jsonpath "$.thid" + +# Holder wait for RequestReceived state +GET {{ agent_url }}/prism-agent/present-proof/presentations +Authorization: Bearer {{ holder_access_token }} +[QueryStringParams] +thid: {{ didcomm_presentation_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].status" == "RequestReceived" +[Captures] +holder_presentation_id: jsonpath "$.contents[0].presentationId" + +# Holder accept presentation request +PATCH {{ agent_url }}/prism-agent/present-proof/presentations/{{ holder_presentation_id }} +Authorization: Bearer {{ holder_access_token }} +{ + "action": "request-accept", + "proofId": ["{{ holder_cred_record_id }}"] +} +HTTP 200 + +# Holder wait for PresentationSent state +GET {{ agent_url }}/prism-agent/present-proof/presentations +Authorization: Bearer {{ holder_access_token }} +[QueryStringParams] +thid: {{ didcomm_presentation_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].status" == "PresentationSent" + +# Verfiier wait for PresentationVerified state +GET {{ agent_url }}/prism-agent/present-proof/presentations +Authorization: Bearer {{ verifier_access_token }} +[QueryStringParams] +thid: {{ didcomm_presentation_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].status" == "PresentationVerified" diff --git a/examples/mt-keycloak/tests/local b/examples/mt-keycloak/tests/local new file mode 100644 index 0000000000..a376aaf58b --- /dev/null +++ b/examples/mt-keycloak/tests/local @@ -0,0 +1,15 @@ +keycloak_admin_password=admin +keycloak_admin_user=admin +keycloak_base_url=http://localhost:8080/keycloak +keycloak_realm=identus + +agent_url=http://localhost:8080 + +issuer_password=1234 +issuer_username=alice + +holder_password=1234 +holder_username=bob + +verifier_password=1234 +verifier_username=john diff --git a/examples/mt/compose.yaml b/examples/mt/compose.yaml new file mode 100644 index 0000000000..84e4f7a3d1 --- /dev/null +++ b/examples/mt/compose.yaml @@ -0,0 +1,114 @@ +configs: + caddyfile_default: + content: |- + :8080 { + handle_path /didcomm* { + reverse_proxy agent-default:8090 + } + handle_path /prism-agent* { + reverse_proxy agent-default:8085 + } + handle_path /keycloak* { + reverse_proxy keycloak-default:8080 + } + handle_path /vault* { + reverse_proxy vault-default:8200 + } + } +services: + agent-default: + depends_on: + node: + condition: service_started + environment: + ADMIN_TOKEN: admin + AGENT_DB_HOST: db-default + AGENT_DB_NAME: agent + AGENT_DB_PASSWORD: postgres + AGENT_DB_PORT: '5432' + AGENT_DB_USER: postgres + API_KEY_ENABLED: 'true' + CONNECT_DB_HOST: db-default + CONNECT_DB_NAME: connect + CONNECT_DB_PASSWORD: postgres + CONNECT_DB_PORT: '5432' + CONNECT_DB_USER: postgres + DIDCOMM_SERVICE_URL: http://caddy-default:8080/didcomm + POLLUX_DB_HOST: db-default + POLLUX_DB_NAME: pollux + POLLUX_DB_PASSWORD: postgres + POLLUX_DB_PORT: '5432' + POLLUX_DB_USER: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-default:8080/prism-agent + PRISM_NODE_HOST: node + PRISM_NODE_PORT: '50053' + REST_SERVICE_URL: http://caddy-default:8080/prism-agent + SECRET_STORAGE_BACKEND: postgres + image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + restart: always + caddy-default: + configs: + - source: caddyfile_default + target: /etc/caddy/Caddyfile + image: caddy:2.7.6-alpine + ports: + - 8080:8080 + restart: always + db-default: + environment: + POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_default:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + node: + depends_on: + node-db: + condition: service_healthy + environment: + NODE_PSQL_DATABASE: node_db + NODE_PSQL_HOST: node-db:5432 + NODE_PSQL_PASSWORD: postgres + NODE_PSQL_USERNAME: postgres + image: ghcr.io/input-output-hk/prism-node:2.4.0 + restart: always + node-db: + environment: + POSTGRES_MULTIPLE_DATABASES: node_db + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_node:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql +volumes: + pg_data_default: {} + pg_data_node: {} diff --git a/examples/readme.md b/examples/readme.md deleted file mode 100644 index 2344b5197c..0000000000 --- a/examples/readme.md +++ /dev/null @@ -1,79 +0,0 @@ -# Examples of OpenAPI specifications -This directory contains OpenAPI/Swagger specifications of Identus competitors. -Not all the companies are real competitors, but number of them are experts in particular area, so we have a good opportunity to get inspiration and build a better solution. - -### godiddy-api.yaml -[Godiddy](https://godiddy.com/) is a hosted platform that makes it easy for SSI developers and solution providers to work with DIDs -It provides the following functionality: -- Resolve DIDs -- Manage DIDs -- Search DIDs - -`Castor` is going to provide slightly similar functionality - -Points to consider: -- routes are grouped by service name -- path convention: {service-name}/{resource}: `/wallet-service/keys` -- each schema contain a prefix of the service name: `WalletService.Key` -- did methods are `verbs`, DIDs is not a collection. The path for methods looks like `RPC` -- `diddocuments` is a collection of all the `versions` of the DID -- wallet service implementation contains path with verbs: `sign` and `verify` - -### aries-cloud-agent.json -[Aries Cloud Agent]() is the SSI solution from Hyperledger - -Hyperledger Aries Cloud Agent Python (ACA-Py) is a foundation for building Verifiable Credential (VC) ecosystems. It operates in the second and third layers of the Trust Over IP framework (PDF) using DIDComm messaging and Hyperledger Aries protocols. The "cloud" in the name means that ACA-Py runs on servers (cloud, enterprise, IoT devices, and so forth), and is not designed to run on mobile devices. - -`Castor`, `Pollux`, `Mercury` are going to provide similar functionality - -Aries Cloud Agent API is a mix of everything you need for SSI in a single place. - -Pros: -- everything is in the single place -- easy to deploy and integrate - -Cons: -- monolith architecture -- probably, it's hard to evolve it because of coupling -- mix of all the agents for Issuer/Verifier/Holder/Mediator/Other APIs - -Points to consider: -- part of the routes are grouped by entity/protocol/service: `did-exchange`, `action-menu`, `mediation` -- part of the routes are started from the root and it's hard to figure out what exactly you are going to do: look at `/connections` -- mix of resource and verbs for protocol facades which is hard to understand: `present-proof`, `issue-credentials` - -### sipca-essif-bridge-api.json -[Essif Bridge](https://github.com/sicpa-dlab/essif-bridge) - -For the BRIDGE-project, SICPA proposes 3 technological building blocks that will enhance interoperability and scalability in the SSI ecosystem by giving freedom of choice between verifiable credentials exchange protocols (DIDcomm & CHAPI), credential types (JSON-LD & Anoncreds) and DID-methods. - -This API documentation is a good example of the integration solution. - -### trinsic-credentials-v1-resolved.json - -[Trinsic](https://docs.trinsic.id/reference/authentication) - -Trinsic is the proof of anything platform. We make it easy for people and organizations to prove things about themselves with technology instead of paper documents. Our software is based on Decentralized Identifiers (DIDs) and Verifiable Credentials (VCs), a new digital identity standard. We use the open-source Hyperledger Aries project, to which we are a primary contributor. (c) - -Trinsic is a competitor of Atala Prism that provides similar functionality. - -It's focused on commodity wallet solution, uses Hyperledger Aries under the hood, provides Open API spec for all the endpoints and SDK in number of languages. - -`Castor`, `Pollux`, `Mercury` are going to provide similar functionality. -Number of Atala products provides similar functionality - -Pros: -- good REST API design -- one level of resources in REST API -- attention to documentation -- solid and simple models without overloading -- should be easy and build the solutions - -Cons: -- simplicity of solution causes lack of `offline` functionality - -Points to consider: - -- Would be nice to have the same quality of API in Atala Prism -- API specification is much simple and engineer-friendly compared to Aries -- Schemas are intuitive, well described and easy-for-quick-start \ No newline at end of file diff --git a/examples/sipca-essif-bridge-api.json b/examples/sipca-essif-bridge-api.json deleted file mode 100644 index e6dc7d123c..0000000000 --- a/examples/sipca-essif-bridge-api.json +++ /dev/null @@ -1,1426 +0,0 @@ -{ - "openapi": "3.0.1", - "info": { - "title": "Sicpa Bridge API", - "version": "0.0.1" - }, - "servers": [ - { - "url": "/api/" - } - ], - "tags": [ - { - "name": "Verifications", - "description": "The Verifications API" - }, - { - "name": "Schemas", - "description": "The Schemas API" - }, - { - "name": "OpenID Connect Connect", - "description": "OpenID Connect API" - }, - { - "name": "Credential Definitions", - "description": "The Credential Definitions API" - }, - { - "name": "JSON-LD Verification", - "description": "Verify a given credential." - }, - { - "name": "Connections", - "description": "The Connections API" - }, - { - "name": "JSON-LD Credentials", - "description": "Issues a JSON-LD credential and returns it in the response body." - }, - { - "name": "Credentials Issuance", - "description": "The Credentials Issuance API" - } - ], - "paths": { - "/verifications": { - "post": { - "tags": [ - "Verifications" - ], - "summary": "Create a new Verification", - "operationId": "verificationsPost", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerificationCreate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Verification" - } - } - } - } - } - } - }, - "/verification-templates": { - "post": { - "tags": [ - "Verifications" - ], - "summary": "Create a new Verification Template", - "operationId": "verificationTemplatesPost", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerificationTemplateCreate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerificationTemplate" - } - } - } - } - } - } - }, - "/schemas": { - "post": { - "tags": [ - "Schemas" - ], - "summary": "Create a new schema", - "operationId": "schemaPost", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SchemaCreate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SchemaSummary" - } - } - } - } - } - } - }, - "/presentations/verify": { - "post": { - "tags": [ - "JSON-LD Verification" - ], - "operationId": "verityCredential", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifiablePresentation" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerificationResult" - } - } - } - } - } - } - }, - "/oidc/sendcredential": { - "post": { - "tags": [ - "OpenID Connect Connect" - ], - "summary": "Sends a credential", - "operationId": "sendCredential", - "requestBody": { - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - } - } - } - }, - "/oidc/didauthresponse/{clientId}": { - "post": { - "tags": [ - "OpenID Connect Connect" - ], - "summary": "Processes Auth Response", - "operationId": "didAuthResponse", - "parameters": [ - { - "name": "clientId", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/AuthResponseCallback" - } - } - } - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - } - } - } - }, - "/oidc/didauthrequest": { - "post": { - "tags": [ - "OpenID Connect Connect" - ], - "summary": "Create a new authenticate url", - "operationId": "didAuthRequest", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthRequestParam" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthRequestResponse" - } - } - } - } - } - } - }, - "/credentials/verify": { - "post": { - "tags": [ - "JSON-LD Verification" - ], - "operationId": "verityCredential_1", - "requestBody": { - "content": { - "application/json": { - "schema": { - "oneOf": [ - { - "$ref": "#/components/schemas/VerifiableCredential" - }, - { - "$ref": "#/components/schemas/VerifiableCredentialMultipleProof" - } - ] - }, - "examples": { - "Verifiable Credential": { - "description": "Verifiable Credential", - "value": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "id": "http://example.gov/credentials/3732", - "type": [ - "VerifiableCredential", - "UniversityDegreeCredential" - ], - "issuer": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - "issuanceDate": "2020-03-16T22:37:26.544Z", - "expirationDate": "2020-03-16T22:37:26.544Z", - "credentialSubject": { - "id": "did:example:123", - "degree": { - "type": "BachelorDegree", - "name": "Bachelor of Science and Arts" - } - }, - "proof": { - "type": "Ed25519Signature2018", - "created": "2020-04-02T18:28:08Z", - "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", - "proofPurpose": "assertionMethod", - "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..YtqjEYnFENT7fNW-COD0HAACxeuQxPKAmp4nIl8jYAu__6IH2FpSxv81w-l5PvE1og50tS9tH8WyXMlXyo45CA" - } - } - }, - "Multiple Proof Verifiable Credential": { - "description": "Multiple Proof Verifiable Credential", - "value": { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "id": "http://example.gov/credentials/3732", - "type": [ - "VerifiableCredential", - "UniversityDegreeCredential" - ], - "issuer": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd", - "issuanceDate": "2020-03-16T22:37:26.544Z", - "expirationDate": "2020-03-16T22:37:26.544Z", - "credentialSubject": { - "id": "did:example:123", - "degree": { - "type": "BachelorDegree", - "name": "Bachelor of Science and Arts" - } - }, - "proof": [ - { - "type": "Ed25519Signature2018", - "created": "2020-04-02T18:28:08Z", - "verificationMethod": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", - "proofPurpose": "assertionMethod", - "jws": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..YtqjEYnFENT7fNW-COD0HAACxeuQxPKAmp4nIl8jYAu__6IH2FpSxv81w-l5PvE1og50tS9tH8WyXMlXyo45CA" - } - ] - } - } - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "Result", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerificationResult" - } - } - } - }, - "400": { - "description": "invalid input!", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "500": { - "description": "error!", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - } - } - } - }, - "/credentials/issue": { - "post": { - "tags": [ - "JSON-LD Credentials" - ], - "operationId": "issueCredential", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/Credential" - } - } - }, - "required": true - }, - "responses": { - "201": { - "description": "Credential successfully issued!", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/VerifiableCredential" - } - } - } - }, - "400": { - "description": "invalid input!", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - }, - "500": { - "description": "error!", - "content": { - "application/json": { - "schema": { - "type": "object" - } - } - } - } - } - } - }, - "/credentials-issuance": { - "post": { - "tags": [ - "Credentials Issuance" - ], - "summary": "Issue new credentials", - "operationId": "issuanceCredentialPost", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialCreate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialIssuance" - } - } - } - } - } - } - }, - "/credential-definitions": { - "post": { - "tags": [ - "Credential Definitions" - ], - "summary": "Create a new credentialDefinition", - "operationId": "credentialDefinitionPost", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialDefinitionCreate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CredentialDefinitionSummary" - } - } - } - } - } - } - }, - "/connections": { - "post": { - "tags": [ - "Connections" - ], - "summary": "Create a new connection invitation", - "operationId": "connectionsPost", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConnectionInvitationCreate" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "OK", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ConnectionInvitationCreationResult" - } - } - } - } - } - } - } - }, - "components": { - "schemas": { - "VerificationCreate": { - "required": [ - "connectionId", - "verificationTemplateId" - ], - "type": "object", - "properties": { - "verificationTemplateId": { - "type": "integer", - "description": "The verification template id", - "format": "int32", - "example": 0 - }, - "connectionId": { - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "type": "string", - "description": "The connection id", - "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" - }, - "comment": { - "type": "string", - "description": "An optional comment" - } - } - }, - "RequestedAttribute": { - "required": [ - "names", - "restrictions" - ], - "type": "object", - "properties": { - "names": { - "type": "array", - "description": "List of attribute names", - "items": { - "type": "string", - "description": "List of attribute names" - } - }, - "restrictions": { - "type": "array", - "description": "The attribute restrictions. At least one restriction should be valid in order for the verification to be successful. (Not considering predicates, revocation requirements...)", - "items": { - "$ref": "#/components/schemas/Restriction" - } - } - }, - "description": "The requested attributes" - }, - "RequestedPredicate": { - "required": [ - "name", - "predicateType", - "predicateValue", - "restrictions" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "A predicate name" - }, - "predicateType": { - "type": "string", - "description": "A predicate type", - "enum": [ - "LESS_THAN", - "LESS_THAN_OR_EQUAL_TO", - "GREATER_THAN_OR_EQUAL_TO", - "GREATER_THAN" - ] - }, - "predicateValue": { - "type": "integer", - "description": "A predicate value", - "format": "int32" - }, - "restrictions": { - "type": "array", - "description": "A list of predicate restrictions", - "items": { - "$ref": "#/components/schemas/Restriction" - } - } - }, - "description": "The requested predicates" - }, - "Restriction": { - "required": [ - "credentialDefinitionId" - ], - "type": "object", - "properties": { - "credentialDefinitionId": { - "type": "string", - "description": "A credential definition ID" - }, - "schemaId": { - "type": "string", - "description": "A schema ID" - } - }, - "description": "A list of predicate restrictions" - }, - "Verification": { - "required": [ - "connectionId", - "createdAt", - "state", - "updatedAt", - "verificationId", - "verificationRequest" - ], - "type": "object", - "properties": { - "verificationId": { - "type": "string", - "description": "The verification id", - "example": "ac32f24e-2f90-4e90-9427-c59482078a4b" - }, - "connectionId": { - "type": "string", - "description": "The connection id", - "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" - }, - "verificationRequest": { - "$ref": "#/components/schemas/VerificationRequest" - }, - "state": { - "type": "string", - "description": "The verification state", - "enum": [ - "SUCCESS", - "PENDING", - "FAILURE", - "ERROR" - ] - }, - "createdAt": { - "type": "string", - "description": "The creation date ", - "format": "date-time" - }, - "updatedAt": { - "type": "string", - "description": "The update date", - "format": "date-time" - } - } - }, - "VerificationRequest": { - "required": [ - "name", - "requestedAttributes", - "requestedPredicates" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The verification request name" - }, - "requestedAttributes": { - "type": "array", - "description": "The requested attributes", - "items": { - "$ref": "#/components/schemas/RequestedAttribute" - } - }, - "requestedPredicates": { - "type": "array", - "description": "The requested predicates", - "items": { - "$ref": "#/components/schemas/RequestedPredicate" - } - }, - "validAt": { - "type": "string", - "description": "The date at which the credential should be valid", - "format": "date-time" - } - }, - "description": "The verification request" - }, - "RevocationRequirement": { - "type": "object", - "properties": { - "validAt": { - "type": "string", - "description": "Date at which the credential should be valid", - "format": "date-time" - }, - "validNow": { - "type": "boolean", - "description": "Defines if the credential should be valid at the current date" - } - } - }, - "VerificationTemplateContent": { - "required": [ - "requestedAttributes", - "requestedPredicates" - ], - "type": "object", - "properties": { - "requestedAttributes": { - "type": "array", - "description": "The requested attributes. In order for the verification to be successful, all requested attribute should be validated", - "items": { - "$ref": "#/components/schemas/RequestedAttribute" - } - }, - "requestedPredicates": { - "type": "array", - "description": "The requested predicates. In order for the verification to be successful, all requested predicates should be validated", - "items": { - "$ref": "#/components/schemas/RequestedPredicate" - } - }, - "revocationRequirement": { - "$ref": "#/components/schemas/RevocationRequirement" - } - } - }, - "VerificationTemplateCreate": { - "required": [ - "content", - "name" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The verificationTemplate name" - }, - "content": { - "$ref": "#/components/schemas/VerificationTemplateContent" - } - } - }, - "VerificationTemplate": { - "required": [ - "content", - "id", - "name" - ], - "type": "object", - "properties": { - "id": { - "type": "integer", - "description": "The verificationTemplate id", - "format": "int32" - }, - "name": { - "type": "string", - "description": "The verificationTemplate name", - "example": "example" - }, - "content": { - "$ref": "#/components/schemas/VerificationTemplateContent" - } - } - }, - "SchemaCreate": { - "required": [ - "attributesName", - "name", - "version" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Schema name", - "example": "prefs" - }, - "version": { - "pattern": "^[0-9.]+$", - "type": "string", - "description": "Schema version", - "example": "1.0" - }, - "attributesName": { - "type": "array", - "description": "List of schema attributes", - "items": { - "type": "string", - "description": "List of schema attributes" - } - } - } - }, - "SchemaSummary": { - "required": [ - "id" - ], - "type": "object", - "properties": { - "id": { - "pattern": "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", - "type": "string", - "description": "The schema's id" - } - } - }, - "LinkedDataProof": { - "required": [ - "created", - "jws", - "proofPurpose", - "type", - "verificationMethod" - ], - "type": "object", - "properties": { - "type": { - "type": "string", - "description": "Linked Data Signature Suite used to produce proof.", - "example": "Ed25519Signature2018" - }, - "created": { - "type": "string", - "description": "Date the proof was created.", - "example": "2020-04-02T18:28:08Z" - }, - "verificationMethod": { - "type": "string", - "description": "Verification Method used to verify proof.", - "example": "did:example:123#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN" - }, - "proofPurpose": { - "type": "string", - "description": "The purpose of the proof to be used with verificationMethod.", - "example": "assertionMethod" - }, - "jws": { - "type": "string", - "description": "Detached JSON Web Signature", - "example": "eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..YtqjEYnFENT7fNW-COD0HAACxeuQxPKAmp4nIl8jYAu__6IH2FpSxv81w-l5PvE1og50tS9tH8WyXMlXyo45CA" - }, - "cades": { - "type": "string" - } - }, - "description": "A JSON-LD Linked Data proof." - }, - "VerifiableCredential": { - "required": [ - "@context", - "credentialSubject", - "issuanceDate", - "issuer", - "proof", - "type" - ], - "type": "object", - "properties": { - "@context": { - "type": "array", - "description": "The JSON-LD context of the credential.", - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "items": { - "type": "string", - "description": "The JSON-LD context of the credential.", - "example": "[\"https://www.w3.org/2018/credentials/v1\",\"https://www.w3.org/2018/credentials/examples/v1\"]" - } - }, - "id": { - "type": "string", - "description": "The ID of the credential.", - "nullable": true, - "example": "http://example.gov/credentials/3732" - }, - "type": { - "type": "array", - "description": "The JSON-LD type of the credential.", - "example": [ - "VerifiableCredential", - "UniversityDegreeCredential" - ], - "items": { - "type": "string", - "description": "The JSON-LD type of the credential.", - "example": "[\"VerifiableCredential\",\"UniversityDegreeCredential\"]" - } - }, - "issuer": { - "type": "string", - "description": "A JSON-LD Verifiable com.sicpa.bridge.api.jsonld.domain.model.Credential Issuer.", - "example": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd" - }, - "issuanceDate": { - "type": "string", - "description": "The issuanceDate", - "example": "2020-03-16T22:37:26.544Z" - }, - "expirationDate": { - "type": "string", - "description": "The expirationDate", - "nullable": true, - "example": "2020-03-16T22:37:26.544Z" - }, - "credentialSubject": { - "type": "object", - "description": "The subject", - "example": { - "id": "did:example:123", - "degree": { - "type": "BachelorDegree", - "name": "Bachelor of Science and Arts" - } - } - }, - "proof": { - "$ref": "#/components/schemas/LinkedDataProof" - } - } - }, - "VerifiablePresentation": { - "required": [ - "@context", - "holder", - "proof", - "type", - "verifiableCredential" - ], - "type": "object", - "properties": { - "@context": { - "type": "array", - "description": "The JSON-LD context of the presentation.", - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "items": { - "type": "string", - "description": "The JSON-LD context of the presentation.", - "example": "[\"https://www.w3.org/2018/credentials/v1\",\"https://www.w3.org/2018/credentials/examples/v1\"]" - } - }, - "holder": { - "type": "string", - "description": "The holder", - "example": "did:example:123" - }, - "type": { - "type": "string", - "description": "The JSON-LD type of the presentation.", - "example": "VerifiablePresentation" - }, - "verifiableCredential": { - "$ref": "#/components/schemas/VerifiableCredential" - }, - "proof": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LinkedDataProof" - } - } - } - }, - "VerificationResult": { - "required": [ - "checks", - "errors", - "warnings" - ], - "type": "object", - "properties": { - "checks": { - "type": "array", - "description": "The checks performed", - "example": [ - "proof" - ], - "items": { - "type": "string", - "description": "The checks performed", - "example": "[\"proof\"]" - } - }, - "warnings": { - "type": "array", - "description": "Warnings", - "example": [], - "items": { - "type": "string", - "description": "Warnings", - "example": "[]" - } - }, - "errors": { - "type": "array", - "description": "Errors", - "example": [], - "items": { - "type": "string", - "description": "Errors", - "example": "[]" - } - }, - "info": { - "type": "object" - } - } - }, - "AuthResponseCallback": { - "required": [ - "id_token", - "login_challenge", - "state" - ], - "type": "object", - "properties": { - "id_token": { - "type": "string" - }, - "state": { - "type": "string" - }, - "login_challenge": { - "type": "string" - } - } - }, - "AuthRequestParam": { - "required": [ - "clientId", - "credentialId" - ], - "type": "object", - "properties": { - "clientId": { - "type": "string" - }, - "credentialId": { - "type": "string" - } - } - }, - "AuthRequestResponse": { - "required": [ - "url" - ], - "type": "object", - "properties": { - "url": { - "type": "string" - } - } - }, - "VerifiableCredentialMultipleProof": { - "required": [ - "@context", - "credentialSubject", - "issuanceDate", - "issuer", - "proof", - "type" - ], - "type": "object", - "properties": { - "@context": { - "type": "array", - "description": "The JSON-LD context of the credential.", - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "items": { - "type": "string", - "description": "The JSON-LD context of the credential.", - "example": "[\"https://www.w3.org/2018/credentials/v1\",\"https://www.w3.org/2018/credentials/examples/v1\"]" - } - }, - "id": { - "type": "string", - "description": "The ID of the credential.", - "nullable": true, - "example": "http://example.gov/credentials/3732" - }, - "type": { - "type": "array", - "description": "The JSON-LD type of the credential.", - "example": [ - "VerifiableCredential", - "UniversityDegreeCredential" - ], - "items": { - "type": "string", - "description": "The JSON-LD type of the credential.", - "example": "[\"VerifiableCredential\",\"UniversityDegreeCredential\"]" - } - }, - "issuer": { - "type": "string", - "description": "A JSON-LD Verifiable com.sicpa.bridge.api.jsonld.domain.model.Credential Issuer.", - "example": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd" - }, - "issuanceDate": { - "type": "string", - "description": "The issuanceDate", - "example": "2020-03-16T22:37:26.544Z" - }, - "expirationDate": { - "type": "string", - "description": "The expirationDate", - "nullable": true, - "example": "2020-03-16T22:37:26.544Z" - }, - "credentialSubject": { - "type": "object", - "description": "The subject", - "example": { - "id": "did:example:123", - "degree": { - "type": "BachelorDegree", - "name": "Bachelor of Science and Arts" - } - } - }, - "proof": { - "type": "array", - "items": { - "$ref": "#/components/schemas/LinkedDataProof" - } - } - } - }, - "Credential": { - "required": [ - "@context", - "credentialSubject", - "issuanceDate", - "issuer", - "type" - ], - "type": "object", - "properties": { - "@context": { - "type": "array", - "description": "The JSON-LD context of the credential.", - "example": [ - "https://www.w3.org/2018/credentials/v1", - "https://www.w3.org/2018/credentials/examples/v1" - ], - "items": { - "type": "string", - "description": "The JSON-LD context of the credential.", - "example": "[\"https://www.w3.org/2018/credentials/v1\",\"https://www.w3.org/2018/credentials/examples/v1\"]" - } - }, - "id": { - "type": "string", - "description": "The ID of the credential.", - "nullable": true, - "example": "http://example.gov/credentials/3732" - }, - "type": { - "type": "array", - "description": "The JSON-LD type of the credential.", - "example": [ - "VerifiableCredential", - "UniversityDegreeCredential" - ], - "items": { - "type": "string", - "description": "The JSON-LD type of the credential.", - "example": "[\"VerifiableCredential\",\"UniversityDegreeCredential\"]" - } - }, - "issuer": { - "type": "string", - "description": "A JSON-LD Verifiable com.sicpa.bridge.api.jsonld.domain.model.Credential Issuer.", - "example": "did:key:z6MkjRagNiMu91DduvCvgEsqLZDVzrJzFrwahc4tXLt9DoHd" - }, - "issuanceDate": { - "type": "string", - "description": "The issuanceDate", - "example": "2020-03-16T22:37:26.544Z" - }, - "credentialSubject": { - "type": "object", - "description": "The subject", - "example": { - "id": "did:example:123", - "degree": { - "type": "BachelorDegree", - "name": "Bachelor of Science and Arts" - } - } - } - } - }, - "CredentialAttribute": { - "required": [ - "name", - "value" - ], - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "A credential attribute name" - }, - "value": { - "type": "string", - "description": "A credential attribute value" - } - }, - "description": "A list of credential attributes" - }, - "CredentialCreate": { - "required": [ - "attributes", - "credentialDefinitionId", - "schemaId" - ], - "type": "object", - "properties": { - "connectionId": { - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "type": "string", - "description": "A connection ID", - "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" - }, - "schemaId": { - "pattern": "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", - "type": "string", - "description": "A schema ID" - }, - "credentialDefinitionId": { - "pattern": "([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", - "type": "string", - "description": "A credential definition ID" - }, - "comment": { - "type": "string", - "description": "A human readable comment" - }, - "attributes": { - "type": "array", - "description": "A list of credential attributes", - "items": { - "$ref": "#/components/schemas/CredentialAttribute" - } - } - } - }, - "CredentialIssuance": { - "required": [ - "credential", - "issuanceId", - "state" - ], - "type": "object", - "properties": { - "issuanceId": { - "type": "string", - "description": "A credential issuance ID" - }, - "errorMessage": { - "type": "string", - "description": "Optional error message" - }, - "state": { - "type": "string", - "description": "State of the credential transaction" - }, - "updatedAt": { - "type": "string", - "format": "date-time" - }, - "credential": { - "$ref": "#/components/schemas/Credential" - } - } - }, - "CredentialDefinitionCreate": { - "required": [ - "schemaId", - "supportRevocation", - "tag" - ], - "type": "object", - "properties": { - "schemaId": { - "pattern": "^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+$", - "type": "string", - "description": "Schema identifier", - "example": "WgWxqztrNooG92RXvxSTWv:2:schema_name:1.0" - }, - "tag": { - "type": "string", - "description": "Credential definition identifier tag", - "example": "default" - }, - "supportRevocation": { - "type": "boolean", - "description": "Revocation supported flag" - } - } - }, - "CredentialDefinitionSummary": { - "required": [ - "id" - ], - "type": "object", - "properties": { - "id": { - "pattern": "([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}):3:CL:(([1-9][0-9]*)|([123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{21,22}:2:.+:[0-9.]+)):(.+)?$", - "type": "string", - "description": "The credential definition's id" - } - } - }, - "ConnectionInvitationCreate": { - "type": "object", - "properties": { - "alias": { - "type": "string", - "description": "Connection alias", - "example": "Some alias" - } - } - }, - "ConnectionInvitationContent": { - "required": [ - "invitationId", - "recipientKeys", - "serviceEndpoint" - ], - "type": "object", - "properties": { - "invitationId": { - "type": "string", - "description": "The connection's invitation id" - }, - "serviceEndpoint": { - "type": "string", - "description": "The connection's service endpoint" - }, - "recipientKeys": { - "type": "array", - "description": "The list of recipient's keys", - "items": { - "type": "string", - "description": "The list of recipient's keys" - } - }, - "label": { - "type": "string", - "description": "The connection's label" - } - } - }, - "ConnectionInvitationCreationResult": { - "required": [ - "connectionId", - "connectionInvitationContent", - "invitationUrl" - ], - "type": "object", - "properties": { - "invitationUrl": { - "type": "string", - "description": "Invitation URL", - "example": "http://192.168.56.101:8020/invite?c_i=eyJAdHlwZSI6Li4ufQ==" - }, - "connectionInvitationContent": { - "$ref": "#/components/schemas/ConnectionInvitationContent" - }, - "connectionId": { - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", - "type": "string" - } - } - } - } - } -} \ No newline at end of file diff --git a/examples/st-multi/compose.yaml b/examples/st-multi/compose.yaml new file mode 100644 index 0000000000..34650ad413 --- /dev/null +++ b/examples/st-multi/compose.yaml @@ -0,0 +1,268 @@ +configs: + caddyfile_holder: + content: |- + :8081 { + handle_path /didcomm* { + reverse_proxy agent-holder:8090 + } + handle_path /prism-agent* { + reverse_proxy agent-holder:8085 + } + handle_path /keycloak* { + reverse_proxy keycloak-holder:8080 + } + handle_path /vault* { + reverse_proxy vault-holder:8200 + } + } + caddyfile_issuer: + content: |- + :8080 { + handle_path /didcomm* { + reverse_proxy agent-issuer:8090 + } + handle_path /prism-agent* { + reverse_proxy agent-issuer:8085 + } + handle_path /keycloak* { + reverse_proxy keycloak-issuer:8080 + } + handle_path /vault* { + reverse_proxy vault-issuer:8200 + } + } + caddyfile_verifier: + content: |- + :8082 { + handle_path /didcomm* { + reverse_proxy agent-verifier:8090 + } + handle_path /prism-agent* { + reverse_proxy agent-verifier:8085 + } + handle_path /keycloak* { + reverse_proxy keycloak-verifier:8080 + } + handle_path /vault* { + reverse_proxy vault-verifier:8200 + } + } +services: + agent-holder: + depends_on: + node: + condition: service_started + environment: + ADMIN_TOKEN: admin + AGENT_DB_HOST: db-holder + AGENT_DB_NAME: agent + AGENT_DB_PASSWORD: postgres + AGENT_DB_PORT: '5432' + AGENT_DB_USER: postgres + API_KEY_ENABLED: 'false' + CONNECT_DB_HOST: db-holder + CONNECT_DB_NAME: connect + CONNECT_DB_PASSWORD: postgres + CONNECT_DB_PORT: '5432' + CONNECT_DB_USER: postgres + DIDCOMM_SERVICE_URL: http://caddy-holder:8081/didcomm + POLLUX_DB_HOST: db-holder + POLLUX_DB_NAME: pollux + POLLUX_DB_PASSWORD: postgres + POLLUX_DB_PORT: '5432' + POLLUX_DB_USER: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-holder:8081/prism-agent + PRISM_NODE_HOST: node + PRISM_NODE_PORT: '50053' + REST_SERVICE_URL: http://caddy-holder:8081/prism-agent + SECRET_STORAGE_BACKEND: postgres + image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + restart: always + agent-issuer: + depends_on: + node: + condition: service_started + environment: + ADMIN_TOKEN: admin + AGENT_DB_HOST: db-issuer + AGENT_DB_NAME: agent + AGENT_DB_PASSWORD: postgres + AGENT_DB_PORT: '5432' + AGENT_DB_USER: postgres + API_KEY_ENABLED: 'false' + CONNECT_DB_HOST: db-issuer + CONNECT_DB_NAME: connect + CONNECT_DB_PASSWORD: postgres + CONNECT_DB_PORT: '5432' + CONNECT_DB_USER: postgres + DIDCOMM_SERVICE_URL: http://caddy-issuer:8080/didcomm + POLLUX_DB_HOST: db-issuer + POLLUX_DB_NAME: pollux + POLLUX_DB_PASSWORD: postgres + POLLUX_DB_PORT: '5432' + POLLUX_DB_USER: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/prism-agent + PRISM_NODE_HOST: node + PRISM_NODE_PORT: '50053' + REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent + SECRET_STORAGE_BACKEND: postgres + image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + restart: always + agent-verifier: + depends_on: + node: + condition: service_started + environment: + ADMIN_TOKEN: admin + AGENT_DB_HOST: db-verifier + AGENT_DB_NAME: agent + AGENT_DB_PASSWORD: postgres + AGENT_DB_PORT: '5432' + AGENT_DB_USER: postgres + API_KEY_ENABLED: 'false' + CONNECT_DB_HOST: db-verifier + CONNECT_DB_NAME: connect + CONNECT_DB_PASSWORD: postgres + CONNECT_DB_PORT: '5432' + CONNECT_DB_USER: postgres + DIDCOMM_SERVICE_URL: http://caddy-verifier:8082/didcomm + POLLUX_DB_HOST: db-verifier + POLLUX_DB_NAME: pollux + POLLUX_DB_PASSWORD: postgres + POLLUX_DB_PORT: '5432' + POLLUX_DB_USER: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-verifier:8082/prism-agent + PRISM_NODE_HOST: node + PRISM_NODE_PORT: '50053' + REST_SERVICE_URL: http://caddy-verifier:8082/prism-agent + SECRET_STORAGE_BACKEND: postgres + image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + restart: always + caddy-holder: + configs: + - source: caddyfile_holder + target: /etc/caddy/Caddyfile + image: caddy:2.7.6-alpine + ports: + - 8081:8081 + restart: always + caddy-issuer: + configs: + - source: caddyfile_issuer + target: /etc/caddy/Caddyfile + image: caddy:2.7.6-alpine + ports: + - 8080:8080 + restart: always + caddy-verifier: + configs: + - source: caddyfile_verifier + target: /etc/caddy/Caddyfile + image: caddy:2.7.6-alpine + ports: + - 8082:8082 + restart: always + db-holder: + environment: + POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_holder:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + db-issuer: + environment: + POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_issuer:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + db-verifier: + environment: + POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_verifier:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + node: + depends_on: + node-db: + condition: service_healthy + environment: + NODE_PSQL_DATABASE: node_db + NODE_PSQL_HOST: node-db:5432 + NODE_PSQL_PASSWORD: postgres + NODE_PSQL_USERNAME: postgres + image: ghcr.io/input-output-hk/prism-node:2.4.0 + restart: always + node-db: + environment: + POSTGRES_MULTIPLE_DATABASES: node_db + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_node:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql +volumes: + pg_data_holder: {} + pg_data_issuer: {} + pg_data_node: {} + pg_data_verifier: {} diff --git a/examples/st-multi/tests/01_jwt_flow.hurl b/examples/st-multi/tests/01_jwt_flow.hurl new file mode 100644 index 0000000000..94bc540275 --- /dev/null +++ b/examples/st-multi/tests/01_jwt_flow.hurl @@ -0,0 +1,237 @@ +############################## +# Prerequisites +############################## +# Issuer create DID +POST {{ issuer_url }}/prism-agent/did-registrar/dids +{ + "documentTemplate": { + "publicKeys": [ + { + "id": "iss-key", + "purpose": "assertionMethod" + } + ], + "services": [] + } +} +HTTP 201 +[Captures] +issuer_did: jsonpath "$.longFormDid" regex "(did:prism:[a-z0-9]+):.+$" + +# Issuer publish DID +POST {{ issuer_url }}/prism-agent/did-registrar/dids/{{ issuer_did }}/publications +HTTP 202 + +# Issuer wait for DID to be published +GET {{ issuer_url }}/prism-agent/did-registrar/dids/{{ issuer_did }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.status" == "PUBLISHED" + +# Holder create DID +POST {{ holder_url }}/prism-agent/did-registrar/dids +{ + "documentTemplate": { + "publicKeys": [ + { + "id": "auth-key", + "purpose": "authentication" + } + ], + "services": [] + } +} +HTTP 201 +[Captures] +holder_did: jsonpath "$.longFormDid" + +############################## +# Issuance Connection +############################## +# Inviter create connection +POST {{ issuer_url }}/prism-agent/connections +{ + "label": "My Connection" +} +HTTP 201 +[Captures] +raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" +issuer_connection_id: jsonpath "$.connectionId" + +# Invitee accept connection +POST {{ holder_url }}/prism-agent/connection-invitations +{ + "invitation": "{{ raw_invitation }}" +} +HTTP 200 +[Captures] +holder_connection_id: jsonpath "$.connectionId" + +# Wait for inviter connection status +GET {{ issuer_url }}/prism-agent/connections/{{ issuer_connection_id }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseSent" + +# Wait for invitee connection status +GET {{ holder_url }}/prism-agent/connections/{{ holder_connection_id }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseReceived" + +############################## +# Issuance +############################## +# Issuer create credential offer +POST {{ issuer_url }}/prism-agent/issue-credentials/credential-offers +{ + "claims": { + "emailAddress": "alice@wonderland.com", + "givenName": "Alice", + "familyName": "Wonderland" + }, + "credentialFormat": "JWT", + "issuingDID": "{{ issuer_did }}", + "connectionId": "{{ issuer_connection_id }}" +} +HTTP 201 +[Captures] +issuer_cred_record_id: jsonpath "$.recordId" +didcomm_issuing_thid: jsonpath "$.thid" + +# Holder wait for OfferReceived state +GET {{ holder_url }}/prism-agent/issue-credentials/records +[QueryStringParams] +thid: {{ didcomm_issuing_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].protocolState" == "OfferReceived" +[Captures] +holder_cred_record_id: jsonpath "$.contents[0].recordId" + +# Holder accept a credential-offer +POST {{ holder_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }}/accept-offer +{ + "subjectId": "{{ holder_did }}" +} +HTTP 200 + +# Holder wait for CredentialReceived state +GET {{ holder_url }}/prism-agent/issue-credentials/records/{{ holder_cred_record_id }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.protocolState" == "CredentialReceived" + +# Issuer wait for CredentialSent state +GET {{ issuer_url }}/prism-agent/issue-credentials/records/{{ issuer_cred_record_id }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.protocolState" == "CredentialSent" + +############################## +# Presentation Connection +############################## +# Inviter create connection +POST {{ verifier_url }}/prism-agent/connections +{ + "label": "My Connection" +} +HTTP 201 +[Captures] +raw_invitation: jsonpath "$.invitation.invitationUrl" regex ".*_oob=(.*)$" +verifier_connection_id: jsonpath "$.connectionId" + +# Invitee accept connection +POST {{ holder_url }}/prism-agent/connection-invitations +{ + "invitation": "{{ raw_invitation }}" +} +HTTP 200 +[Captures] +holder_connection_id: jsonpath "$.connectionId" + +# Wait for inviter connection status +GET {{ verifier_url }}/prism-agent/connections/{{ verifier_connection_id }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseSent" + +# Wait for invitee connection status +GET {{ holder_url }}/prism-agent/connections/{{ holder_connection_id }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.state" == "ConnectionResponseReceived" + +############################## +# Presentation +############################## +# Verifier create presentation request +POST {{ verifier_url }}/prism-agent/present-proof/presentations +{ + "connectionId": "{{ verifier_connection_id }}", + "proofs":[], + "options": { + "challenge": "11c91493-01b3-4c4d-ac36-b336bab5bddf", + "domain": "https://prism-verifier.com" + } +} +HTTP 201 +[Captures] +verifier_presentation_id: jsonpath "$.presentationId" +didcomm_presentation_thid: jsonpath "$.thid" + +# Holder wait for RequestReceived state +GET {{ holder_url }}/prism-agent/present-proof/presentations +[QueryStringParams] +thid: {{ didcomm_presentation_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].status" == "RequestReceived" +[Captures] +holder_presentation_id: jsonpath "$.contents[0].presentationId" + +# Holder accept presentation request +PATCH {{ holder_url }}/prism-agent/present-proof/presentations/{{ holder_presentation_id }} +{ + "action": "request-accept", + "proofId": ["{{ holder_cred_record_id }}"] +} +HTTP 200 + +# Holder wait for PresentationSent state +GET {{ holder_url }}/prism-agent/present-proof/presentations +[QueryStringParams] +thid: {{ didcomm_presentation_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].status" == "PresentationSent" + +# Verfiier wait for PresentationVerified state +GET {{ verifier_url }}/prism-agent/present-proof/presentations +[QueryStringParams] +thid: {{ didcomm_presentation_thid }} +[Options] +retry: -1 +HTTP 200 +[Asserts] +jsonpath "$.contents[0].status" == "PresentationVerified" diff --git a/examples/st-multi/tests/local b/examples/st-multi/tests/local new file mode 100644 index 0000000000..b7f6b17a33 --- /dev/null +++ b/examples/st-multi/tests/local @@ -0,0 +1,3 @@ +holder_url=http://localhost:8081 +issuer_url=http://localhost:8080 +verifier_url=http://localhost:8082 diff --git a/examples/st-oid4vci/README.md b/examples/st-oid4vci/README.md new file mode 100644 index 0000000000..85b357d0a3 --- /dev/null +++ b/examples/st-oid4vci/README.md @@ -0,0 +1,43 @@ +# How to run issuance flow + +## Prerequisites + +- Docker installed v2.24.0 or later +- Python 3 with the following packages installed + - [requests](https://pypi.org/project/requests/) + - [pyjwt](https://pyjwt.readthedocs.io/en/stable/) + - [cryptography](https://cryptography.io/en/latest/) +- Virtual environment (optional) + +Example of the script to install the required packages in a virtual environment: +```shell +python -m venv {path-to-the-project-dir}/identus-cloud-agent/examples/st-oid4vci/python-env +source {path-to-the-project-dir}/identus-cloud-agent/examples/st-oid4vci/python-env/bin/activate +pip install requests pyjwt cryptography +``` + +- the latest Cloud Agent image is built and available in the local Docker registry + +```shell +sbt docker:publishLocal +``` + +### 1. Spin up the agent stack with pre-configured Keycloak + +```bash +docker-compose up +``` + +The Keycloak UI is available at `http://localhost:9980` and the admin username is `admin` with password `admin`. + +### 2. Run the issuance demo script + +```bash +python demo.py +``` + +- 2.1 Follow the instructions in the terminal. The holder will then be asked to log in via a browser +- 2.2 Enter the username `alice` and password `1234` to log in +- 2.3 Grant access for the scopes displayed on the consent UI + +The credential should be issued at the end of the flow and logged to the terminal. diff --git a/examples/st-oid4vci/bootstrap/01_init_realm.hurl b/examples/st-oid4vci/bootstrap/01_init_realm.hurl new file mode 100644 index 0000000000..9caac55ff4 --- /dev/null +++ b/examples/st-oid4vci/bootstrap/01_init_realm.hurl @@ -0,0 +1,90 @@ +# Wait for keycloak ready +GET {{ keycloak_base_url }}/health/ready +[Options] +retry: 300 +HTTP 200 + +# Admin login +POST {{ keycloak_base_url }}/realms/master/protocol/openid-connect/token +[FormParams] +grant_type: password +client_id: admin-cli +username: {{ keycloak_admin_user }} +password: {{ keycloak_admin_password }} +[Options] +retry: 30 +HTTP 200 +[Captures] +admin_access_token: jsonpath "$.access_token" + +# Create realm +POST {{ keycloak_base_url }}/admin/realms +authorization: Bearer {{ admin_access_token }} +{ + "realm": "{{ keycloak_realm }}", + "enabled": true +} +HTTP 201 + +# Create agent client +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients +Authorization: Bearer {{ admin_access_token }} +{ + "id": "{{ agent_client_id }}", + "authorizationServicesEnabled": true, + "serviceAccountsEnabled": true, + "secret": "{{ agent_client_secret }}" +} +HTTP 201 + +# Create Alice +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/users +Authorization: Bearer {{ admin_access_token }} +{ + "username": "{{ alice_username }}", + "firstName": "Alice", + "lastName": "Wonderland", + "enabled": true, + "email": "alice@atalaprism.io", + "credentials": [{"value": "{{ alice_password }}", "temporary": false}] +} +HTTP 201 + +############################## +# TODO: actions below to be performed by controller +############################## +# Pre-register holder wallet client // TODO: dynamic registration? +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients +Authorization: Bearer {{ admin_access_token }} +{ + "id": "{{ alice_wallet_client_id }}", + "publicClient": true, + "consentRequired": true, + "redirectUris": [ "http://localhost:7777/*" ] +} +HTTP 201 + +# Create a scope for issuable credential +POST {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/client-scopes +Authorization: Bearer {{ admin_access_token }} +{ + "name": "UniversityDegreeCredential", + "description": "The University Degree Credential", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "University Degree", + "display.on.consent.screen": "true", + "include.in.token.scope": "true", + "gui.order": "" + } +} +HTTP 201 +[Captures] +client_scope_id: header "Location" split "/" nth 7 + +# scope mapping +PUT {{ keycloak_base_url }}/admin/realms/{{ keycloak_realm }}/clients/{{ alice_wallet_client_id }}/optional-client-scopes/{{ client_scope_id }} +Authorization: Bearer {{ admin_access_token }} +{} +HTTP 204 + diff --git a/examples/st-oid4vci/compose.yaml b/examples/st-oid4vci/compose.yaml new file mode 100644 index 0000000000..dde0a828ff --- /dev/null +++ b/examples/st-oid4vci/compose.yaml @@ -0,0 +1,151 @@ +configs: + caddyfile_issuer: + content: |- + :8080 { + handle_path /didcomm* { + reverse_proxy agent-issuer:8090 + } + handle_path /prism-agent* { + reverse_proxy agent-issuer:8085 + } + handle_path /keycloak* { + reverse_proxy keycloak-issuer:8080 + } + handle_path /vault* { + reverse_proxy vault-issuer:8200 + } + } +services: + agent-issuer: + depends_on: + node: + condition: service_started + environment: + ADMIN_TOKEN: admin + AGENT_DB_HOST: db-issuer + AGENT_DB_NAME: agent + AGENT_DB_PASSWORD: postgres + AGENT_DB_PORT: '5432' + AGENT_DB_USER: postgres + API_KEY_ENABLED: 'false' + CONNECT_DB_HOST: db-issuer + CONNECT_DB_NAME: connect + CONNECT_DB_PASSWORD: postgres + CONNECT_DB_PORT: '5432' + CONNECT_DB_USER: postgres + DIDCOMM_SERVICE_URL: http://caddy-issuer:8080/didcomm + POLLUX_DB_HOST: db-issuer + POLLUX_DB_NAME: pollux + POLLUX_DB_PASSWORD: postgres + POLLUX_DB_PORT: '5432' + POLLUX_DB_USER: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/prism-agent + PRISM_NODE_HOST: node + PRISM_NODE_PORT: '50053' + REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent + SECRET_STORAGE_BACKEND: postgres + image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + restart: always + caddy-issuer: + configs: + - source: caddyfile_issuer + target: /etc/caddy/Caddyfile + image: caddy:2.7.6-alpine + ports: + - 8080:8080 + restart: always + db-issuer: + environment: + POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_issuer:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + external-keycloak-init-issuer: + command: + - --glob + - /hurl/*.hurl + - --test + environment: + HURL_agent_client_id: cloud-agent + HURL_agent_client_secret: secret + HURL_alice_password: '1234' + HURL_alice_username: alice + HURL_alice_wallet_client_id: alice-wallet + HURL_keycloak_admin_password: admin + HURL_keycloak_admin_user: admin + HURL_keycloak_base_url: http://external-keycloak-issuer:8080 + HURL_keycloak_realm: students + image: ghcr.io/orange-opensource/hurl:4.2.0 + volumes: + - ./bootstrap:/hurl + external-keycloak-issuer: + command: + - start-dev + - --features=preview + - --health-enabled=true + - --hostname-url=http://localhost:9980 + - --hostname-admin-url=http://localhost:9980 + environment: + IDENTUS_URL: http://caddy-issuer:8080/prism-agent + KEYCLOAK_ADMIN: admin + KEYCLOAK_ADMIN_PASSWORD: admin + image: ghcr.io/hyperledger/identus-keycloak-plugins:0.1.0 + ports: + - 9980:8080 + restart: always + mockserver: + image: mockserver/mockserver:5.15.0 + ports: + - 7777:1080 + node: + depends_on: + node-db: + condition: service_healthy + environment: + NODE_PSQL_DATABASE: node_db + NODE_PSQL_HOST: node-db:5432 + NODE_PSQL_PASSWORD: postgres + NODE_PSQL_USERNAME: postgres + image: ghcr.io/input-output-hk/prism-node:2.4.0 + restart: always + node-db: + environment: + POSTGRES_MULTIPLE_DATABASES: node_db + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_node:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql +volumes: + pg_data_issuer: {} + pg_data_node: {} diff --git a/examples/st-oid4vci/demo.py b/examples/st-oid4vci/demo.py new file mode 100755 index 0000000000..0a1b97121e --- /dev/null +++ b/examples/st-oid4vci/demo.py @@ -0,0 +1,314 @@ +import json +import jwt +import requests +import time +import urllib + +from cryptography.hazmat.primitives.asymmetric import ec +from cryptography.hazmat.primitives import serialization + + +MOCKSERVER_URL = "http://localhost:7777" +LOGIN_REDIRECT_URL = "http://localhost:7777/cb" + +AGENT_URL = "http://localhost:8080/prism-agent" +CREDENTIAL_ISSUER = None +CREDENTIAL_ISSUER_DID = None +CREDENTIAL_CONFIGURATION_ID = "UniversityDegreeCredential" +AUTHORIZATION_SERVER = "http://localhost:9980/realms/students" + +ALICE_CLIENT_ID = "alice-wallet" + +HOLDER_LONG_FORM_DID = "did:prism:73196107e806b084d44339c847a3ae8dd279562f23895583f62cc91a2ee5b8fe:CnsKeRI8CghtYXN0ZXItMBABSi4KCXNlY3AyNTZrMRIhArrplJNfQYxthryRU87XdODy-YWUh5mqrvIfAdoZFeJBEjkKBWtleS0wEAJKLgoJc2VjcDI1NmsxEiEC8rsFplfYvRLazdWWi3LNR1gaAQXb-adVhZacJT4ntwE" +HOLDER_ASSERTION_PRIVATE_KEY_HEX = ( + "2902637d412190fb08f5d0e0b2efc1eefae8060ae151e7951b69afbecbdd452e" +) + + +def prepare_mock_server(): + # reset mock server + requests.put(f"{MOCKSERVER_URL}/mockserver/reset") + + # mock wallet authorization callback endpoint + requests.put( + f"{MOCKSERVER_URL}/mockserver/expectation", + json={ + "httpRequest": {"path": "/cb"}, + "httpResponse": { + "statusCode": 200, + "body": {"type": "string", "string": "Login Successful"}, + }, + }, + ) + + +def prepare_issuer(): + # prepare issuging DID + dids = requests.get(f"{AGENT_URL}/did-registrar/dids").json()["contents"] + if len(dids) == 0: + requests.post( + f"{AGENT_URL}/did-registrar/dids", + json={ + "documentTemplate": { + "publicKeys": [{"id": "iss", "purpose": "assertionMethod"}], + "services": [], + } + }, + ) + dids = requests.get(f"{AGENT_URL}/did-registrar/dids").json()["contents"] + + issuer_did = dids[0] + while issuer_did["status"] != "PUBLISHED": + time.sleep(2) + canonical_did = issuer_did["did"] + issuer_did = requests.get( + f"{AGENT_URL}/did-registrar/dids/{canonical_did}" + ).json() + + # publish if not pending + if issuer_did["status"] == "CREATED": + requests.post( + f"{AGENT_URL}/did-registrar/dids/{canonical_did}/publications" + ) + canonical_did = issuer_did["did"] + global CREDENTIAL_ISSUER_DID + CREDENTIAL_ISSUER_DID = canonical_did + + # prepare schema + schema = requests.post( + f"{AGENT_URL}/schema-registry/schemas", + json={ + "name": "UniversityDegree", + "version": "1.0.0", + "type": "https://w3c-ccg.github.io/vc-json-schemas/schema/2.0/schema.json", + "schema": { + "$id": "https://example.com/driving-license-1.0", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "firstName": {"type": "string"}, + "degree": {"type": "string"}, + "grade": {"type": "number"}, + }, + "required": ["firstName", "grade"], + "additionalProperties": False, + }, + "tags": [], + "author": canonical_did, + }, + ).json() + schema_guid = schema["guid"] + + # prepare issuer + credential_issuer = requests.post( + f"{AGENT_URL}/oid4vci/issuers", + json={ + "authorizationServer": { + "url": "http://external-keycloak-issuer:8080/realms/students", + "clientId": "cloud-agent", + "clientSecret": "secret", + } + }, + ).json() + issuer_id = credential_issuer["id"] + global CREDENTIAL_ISSUER + CREDENTIAL_ISSUER = f"{AGENT_URL}/oid4vci/issuers/{issuer_id}" + + # prepare credential configuration + cred_config = requests.post( + f"{CREDENTIAL_ISSUER}/credential-configurations", + json={ + "configurationId": CREDENTIAL_CONFIGURATION_ID, + "format": "jwt_vc_json", + # TODO: align docker host URL + "schemaId": f"http://localhost:8085/schema-registry/schemas/{schema_guid}/schema", + }, + ).json() + + +def issuer_create_credential_offer(claims): + response = requests.post( + f"{CREDENTIAL_ISSUER}/credential-offers", + json={ + "credentialConfigurationId": CREDENTIAL_CONFIGURATION_ID, + "issuingDID": CREDENTIAL_ISSUER_DID, + "claims": claims, + }, + ) + return response.json()["credentialOffer"] + + +def holder_get_issuer_metadata(credential_issuer: str): + # FIXME: override this just to make url reachable from host + def override_host(url): + return url.replace("http://caddy-issuer:8080/prism-agent", AGENT_URL) + + metadata_url = override_host( + f"{credential_issuer}/.well-known/openid-credential-issuer" + ) + response = requests.get(metadata_url).json() + response["credential_endpoint"] = override_host(response["credential_endpoint"]) + response["credential_issuer"] = override_host(response["credential_issuer"]) + response["authorization_servers"] = [ + AUTHORIZATION_SERVER for s in response["authorization_servers"] + ] + return response + + +def holder_get_issuer_as_metadata(authorization_server: str): + metadata_url = f"{authorization_server}/.well-known/openid-configuration" + response = requests.get(metadata_url) + metadata = response.json() + print(f"Metadata: {metadata}") + return metadata + + +def holder_start_login_flow(auth_endpoint: str, token_endpoint: str, issuer_state: str): + def wait_redirect_authorization_code() -> str: + print("wating for authorization redirect ...") + while True: + response = requests.put( + f"{MOCKSERVER_URL}/mockserver/retrieve?type=REQUESTS", + json={"path": "/cb", "method": "GET"}, + ).json() + if len(response) > 0: + break + time.sleep(1) + + authorzation_code = response[0]["queryStringParameters"]["code"][0] + print(f"code: {authorzation_code}") + return authorzation_code + + def start_authorization_request(auth_endpoint: str, issuer_state: str): + # Authorization Request + queries = urllib.parse.urlencode( + { + "redirect_uri": LOGIN_REDIRECT_URL, + "response_type": "code", + "client_id": ALICE_CLIENT_ID, + "scope": "openid " + CREDENTIAL_CONFIGURATION_ID, + "issuer_state": issuer_state, + } + ) + login_url = f"{auth_endpoint}?{queries}" + print("\n##############################\n") + print("Open this link in the browser to login\n") + print(login_url) + print("\n##############################\n") + + # wait for authorization redirect + authorzation_code = wait_redirect_authorization_code() + return authorzation_code + + def start_token_request(token_endpoint: str, authorization_code: str): + # Token Request + response = requests.post( + token_endpoint, + data={ + "grant_type": "authorization_code", + "code": authorization_code, + "client_id": ALICE_CLIENT_ID, + "redirect_uri": LOGIN_REDIRECT_URL, + }, + ) + return response.json() + + authorization_code = start_authorization_request(auth_endpoint, issuer_state) + token_response = start_token_request(token_endpoint, authorization_code) + return token_response + + +def holder_extract_credential_offer(offer_uri: str): + queries = urllib.parse.urlparse(credential_offer_uri).query + credential_offer = urllib.parse.parse_qs(queries)["credential_offer"] + return json.loads(credential_offer[0]) + + +def holder_get_credential(credential_endpoint: str, token_response): + access_token = token_response["access_token"] + c_nonce = token_response["c_nonce"] + + # generate proof + private_key_bytes = bytes.fromhex(HOLDER_ASSERTION_PRIVATE_KEY_HEX) + private_key = ec.derive_private_key( + int.from_bytes(private_key_bytes, "big"), ec.SECP256K1() + ) + public_key = private_key.public_key() + jwt_proof = jwt.encode( + headers={ + "typ": "openid4vci-proof+jwt", + "kid": HOLDER_LONG_FORM_DID, + }, + payload={ + "iss": ALICE_CLIENT_ID, + "aud": CREDENTIAL_ISSUER, + "iat": int(time.time()), + "nonce": c_nonce, + }, + key=private_key, + algorithm="ES256K", # TODO: switch to EdDSA alg (Ed25519) + ) + + response = requests.post( + credential_endpoint, + headers={"Authorization": f"Bearer {access_token}"}, + json={ + "format": "jwt_vc_json", + "credential_definition": { + "type": ["VerifiableCredential", CREDENTIAL_CONFIGURATION_ID], + "credentialSubject": {}, + }, + "proof": {"proof_type": "jwt", "jwt": jwt_proof}, + }, + ) + return response.json() + + +if __name__ == "__main__": + prepare_mock_server() + prepare_issuer() + + # step 1: Issuer create CredentialOffer + credential_offer_uri = issuer_create_credential_offer( + {"firstName": "Alice", "degree": "ChemicalEngineering", "grade": 3.2} + ) + + # step 2: Issuer present QR code container CredentialOffer URI + credential_offer = holder_extract_credential_offer(credential_offer_uri) + credential_offer_pretty = json.dumps(credential_offer, indent=2) + issuer_state = credential_offer["grants"]["authorization_code"]["issuer_state"] + print("\n##############################\n") + print(f"QR code scanned, got credential-offer\n\n{credential_offer_uri}\n") + print(f"\n{credential_offer_pretty}\n") + print("\n##############################\n") + input("\nEnter to continue ...") + + # step 3: Holdler retreive Issuer's metadata + issuer_metadata = holder_get_issuer_metadata(CREDENTIAL_ISSUER) + authorzation_server = issuer_metadata["authorization_servers"][0] + print("\n::::: Issuer Metadata :::::") + print(json.dumps(issuer_metadata, indent=2)) + input("\nEnter to continue ...") + + # step 3.1: Holder retreive Issuer's AS metadata + issuer_as_metadata = holder_get_issuer_as_metadata(authorzation_server) + issuer_as_token_endpoint = issuer_as_metadata["token_endpoint"] + issuer_as_authorization_endpoint = issuer_as_metadata["authorization_endpoint"] + print("\n::::: Issuer Authorization Server Metadata :::::") + print(f"issuer_as_auth_endpoint: {issuer_as_authorization_endpoint}") + print(f"issuer_as_token_endpoint: {issuer_as_token_endpoint}") + input("\nEnter to continue ...") + + # step 4: Holder start authorization flow + token_response = holder_start_login_flow( + issuer_as_authorization_endpoint, issuer_as_token_endpoint, issuer_state + ) + print("::::: Token Response :::::") + print(json.dumps(token_response, indent=2)) + input("\nEnter to continue ...") + + # step 5: Holder use access_token to get credential + credential_endpoint = issuer_metadata["credential_endpoint"] + jwt_credential = holder_get_credential(credential_endpoint, token_response) + print("\n::::: Credential Received :::::") + print(json.dumps(jwt_credential, indent=2)) diff --git a/examples/st-vault/compose.yaml b/examples/st-vault/compose.yaml new file mode 100644 index 0000000000..911a4a1f42 --- /dev/null +++ b/examples/st-vault/compose.yaml @@ -0,0 +1,133 @@ +configs: + caddyfile_issuer: + content: |- + :8080 { + handle_path /didcomm* { + reverse_proxy agent-issuer:8090 + } + handle_path /prism-agent* { + reverse_proxy agent-issuer:8085 + } + handle_path /keycloak* { + reverse_proxy keycloak-issuer:8080 + } + handle_path /vault* { + reverse_proxy vault-issuer:8200 + } + } +services: + agent-issuer: + depends_on: + node: + condition: service_started + environment: + ADMIN_TOKEN: admin + AGENT_DB_HOST: db-issuer + AGENT_DB_NAME: agent + AGENT_DB_PASSWORD: postgres + AGENT_DB_PORT: '5432' + AGENT_DB_USER: postgres + API_KEY_ENABLED: 'false' + CONNECT_DB_HOST: db-issuer + CONNECT_DB_NAME: connect + CONNECT_DB_PASSWORD: postgres + CONNECT_DB_PORT: '5432' + CONNECT_DB_USER: postgres + DIDCOMM_SERVICE_URL: http://caddy-issuer:8080/didcomm + POLLUX_DB_HOST: db-issuer + POLLUX_DB_NAME: pollux + POLLUX_DB_PASSWORD: postgres + POLLUX_DB_PORT: '5432' + POLLUX_DB_USER: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/prism-agent + PRISM_NODE_HOST: node + PRISM_NODE_PORT: '50053' + REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent + SECRET_STORAGE_BACKEND: vault + VAULT_ADDR: http://vault-issuer:8200 + VAULT_TOKEN: admin + image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + restart: always + caddy-issuer: + configs: + - source: caddyfile_issuer + target: /etc/caddy/Caddyfile + image: caddy:2.7.6-alpine + ports: + - 8080:8080 + restart: always + db-issuer: + environment: + POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_issuer:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + node: + depends_on: + node-db: + condition: service_healthy + environment: + NODE_PSQL_DATABASE: node_db + NODE_PSQL_HOST: node-db:5432 + NODE_PSQL_PASSWORD: postgres + NODE_PSQL_USERNAME: postgres + image: ghcr.io/input-output-hk/prism-node:2.4.0 + restart: always + node-db: + environment: + POSTGRES_MULTIPLE_DATABASES: node_db + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_node:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + vault-issuer: + cap_add: + - IPC_LOCK + environment: + VAULT_ADDR: http://0.0.0.0:8200 + VAULT_DEV_ROOT_TOKEN_ID: admin + healthcheck: + interval: 10s + retries: '5' + test: + - CMD + - vault + - status + timeout: 5s + image: hashicorp/vault:1.15.6 + ports: + - 8200:8200 +volumes: + pg_data_issuer: {} + pg_data_node: {} diff --git a/examples/st/compose.yaml b/examples/st/compose.yaml new file mode 100644 index 0000000000..167a2a199e --- /dev/null +++ b/examples/st/compose.yaml @@ -0,0 +1,114 @@ +configs: + caddyfile_issuer: + content: |- + :8080 { + handle_path /didcomm* { + reverse_proxy agent-issuer:8090 + } + handle_path /prism-agent* { + reverse_proxy agent-issuer:8085 + } + handle_path /keycloak* { + reverse_proxy keycloak-issuer:8080 + } + handle_path /vault* { + reverse_proxy vault-issuer:8200 + } + } +services: + agent-issuer: + depends_on: + node: + condition: service_started + environment: + ADMIN_TOKEN: admin + AGENT_DB_HOST: db-issuer + AGENT_DB_NAME: agent + AGENT_DB_PASSWORD: postgres + AGENT_DB_PORT: '5432' + AGENT_DB_USER: postgres + API_KEY_ENABLED: 'false' + CONNECT_DB_HOST: db-issuer + CONNECT_DB_NAME: connect + CONNECT_DB_PASSWORD: postgres + CONNECT_DB_PORT: '5432' + CONNECT_DB_USER: postgres + DIDCOMM_SERVICE_URL: http://caddy-issuer:8080/didcomm + POLLUX_DB_HOST: db-issuer + POLLUX_DB_NAME: pollux + POLLUX_DB_PASSWORD: postgres + POLLUX_DB_PORT: '5432' + POLLUX_DB_USER: postgres + POLLUX_STATUS_LIST_REGISTRY_PUBLIC_URL: http://caddy-issuer:8080/prism-agent + PRISM_NODE_HOST: node + PRISM_NODE_PORT: '50053' + REST_SERVICE_URL: http://caddy-issuer:8080/prism-agent + SECRET_STORAGE_BACKEND: postgres + image: ghcr.io/hyperledger/identus-cloud-agent:1.36.1-SNAPSHOT + restart: always + caddy-issuer: + configs: + - source: caddyfile_issuer + target: /etc/caddy/Caddyfile + image: caddy:2.7.6-alpine + ports: + - 8080:8080 + restart: always + db-issuer: + environment: + POSTGRES_MULTIPLE_DATABASES: pollux,connect,agent + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_issuer:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql + node: + depends_on: + node-db: + condition: service_healthy + environment: + NODE_PSQL_DATABASE: node_db + NODE_PSQL_HOST: node-db:5432 + NODE_PSQL_PASSWORD: postgres + NODE_PSQL_USERNAME: postgres + image: ghcr.io/input-output-hk/prism-node:2.4.0 + restart: always + node-db: + environment: + POSTGRES_MULTIPLE_DATABASES: node_db + POSTGRES_PASSWORD: postgres + POSTGRES_USER: postgres + healthcheck: + interval: 10s + retries: 5 + test: + - CMD + - pg_isready + - -U + - postgres + - -d + - postgres + timeout: 5s + image: postgres:13 + restart: always + volumes: + - pg_data_node:/var/lib/postgresql/data + - ../.shared/postgres/init-script.sh:/docker-entrypoint-initdb.d/init-script.sh + - ../.shared/postgres/max_conns.sql:/docker-entrypoint-initdb.d/max_conns.sql +volumes: + pg_data_issuer: {} + pg_data_node: {} diff --git a/examples/trinsic-credentials-v1-resolved.json b/examples/trinsic-credentials-v1-resolved.json deleted file mode 100644 index 643e777a51..0000000000 --- a/examples/trinsic-credentials-v1-resolved.json +++ /dev/null @@ -1,3039 +0,0 @@ -{ - "swagger": "2.0", - "info": { - "description": "An API to issue, manage, and verify digital credentials", - "version": "v1", - "title": "Credentials API", - "termsOfService": "https://trinsic.id/terms/", - "contact": { - "name": "Trinsic Engineering Team", - "url": "https://trinsic.id/contact-us", - "email": "support@trinsic.id" - } - }, - "host": "api.trinsic.id", - "basePath": "/credentials/v1", - "schemes": [ - "https" - ], - "security": [ - { - "oauth2": [] - } - ], - "paths": { - "/common/upload": { - "post": { - "tags": [ - "Common" - ], - "summary": "[Deprecated] Upload image", - "description": "Please use the Provider API instead.\r\nUpload an image and return a URL with the static remote location.", - "operationId": "UploadImage", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "uploadedFiles", - "in": "formData", - "required": true, - "type": "file" - }, - { - "name": "filename", - "in": "formData", - "required": false, - "type": "string" - }, - { - "name": "contentType", - "in": "formData", - "required": false, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "string", - "format": "uri" - } - } - } - } - }, - "/common/networks": { - "get": { - "tags": [ - "Common" - ], - "summary": "[Deprecated] List all ledger networks", - "description": "Please use the Provider API instead.\r\nList all available ledger networks.\r\nSome networks are not available based on your subscription.", - "operationId": "ListNetworks", - "produces": [ - "application/json" - ], - "parameters": [], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/NetworkContract" - } - } - } - } - } - }, - "/common/networks/{networkId}/txnAuthorAgreement": { - "get": { - "tags": [ - "Common" - ], - "summary": "[Deprecated] Get network transaction author agreement", - "description": "This endpoint is no longer needed.\r\nGet the latest transaction author agreement and acceptance methods if one is set on the specified network.", - "operationId": "GetTransactionAuthorAgreement", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "networkId", - "in": "path", - "description": "The network identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/NetworkTxnAgreementContract" - } - } - } - } - }, - "/common/networks/{tenantId}/txnAuthorAgreement": { - "put": { - "tags": [ - "Common" - ], - "summary": "[Deprecated] Accept network transaction author agreement", - "description": "This endpoint is no longer needed.\r\nAccept the latest transaction author agreement on the specified network.", - "operationId": "AcceptTransactionAuthorAgreement", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "The tenant identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/connections": { - "get": { - "tags": [ - "Connections" - ], - "summary": "List all connections", - "description": "Retrieve a list of all connections.\r\nOptionally, list only connections in a specified state.", - "operationId": "ListConnections", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "state", - "in": "query", - "description": "The connection state", - "required": false, - "type": "string", - "enum": [ - "Invited", - "Negotiating", - "Connected" - ] - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/ConnectionContract" - } - } - } - } - }, - "post": { - "tags": [ - "Connections" - ], - "summary": "Create connection", - "description": "Initiate a new connection by creating an invitation.\r\nThe newly created connection record will be in state 'Invited' until the other party has accepted the invitation. \r\nThe response body includes details about the newly creation connection.", - "operationId": "CreateConnection", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Connection invitation parameters", - "required": true, - "schema": { - "$ref": "#/definitions/ConnectionInvitationParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/ConnectionContract" - } - } - } - } - }, - "/connections/{connectionId}": { - "get": { - "tags": [ - "Connections" - ], - "summary": "Get connection", - "description": "Get the specified connection.", - "operationId": "GetConnection", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "connectionId", - "in": "path", - "description": "The connection identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/ConnectionContract" - } - } - } - }, - "delete": { - "tags": [ - "Connections" - ], - "summary": "Delete connection", - "description": "Delete the specified connection.", - "operationId": "DeleteConnection", - "parameters": [ - { - "name": "connectionId", - "in": "path", - "description": "The connection identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/credentials": { - "get": { - "tags": [ - "Credentials" - ], - "summary": "List all credentials", - "description": "List all credentials that match any specified query parameters.\r\nNo query parameters are required, but any provided will filter the results.", - "operationId": "ListCredentials", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "connectionId", - "in": "query", - "description": "A connection identifier", - "required": false, - "type": "string" - }, - { - "name": "state", - "in": "query", - "description": "The state of credentials", - "required": false, - "type": "string", - "enum": [ - "Offered", - "Requested", - "Issued", - "Rejected", - "Revoked" - ] - }, - { - "name": "definitionId", - "in": "query", - "description": "A credential definition identifier", - "required": false, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/CredentialContract" - } - } - } - } - }, - "post": { - "tags": [ - "Credentials" - ], - "summary": "Create and offer credential", - "description": "Send a credential offer of the specified credential definition to the specified connection.", - "operationId": "CreateCredential", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "The definition and connection to which this offer will be sent", - "required": true, - "schema": { - "$ref": "#/definitions/CredentialOfferParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/CredentialContract" - } - } - } - } - }, - "/credentials/{credentialId}": { - "get": { - "tags": [ - "Credentials" - ], - "summary": "Get credential", - "description": "Get the specified credential.", - "operationId": "GetCredential", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "credentialId", - "in": "path", - "description": "The credential identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/CredentialContract" - } - } - } - }, - "put": { - "tags": [ - "Credentials" - ], - "summary": "Issue credential", - "description": "Issue the specified credential.\r\nIf the values offered were incorrect, changes to the values may be made here.\r\nYou must update all of the values, and they must be follow the same structure of the schema.\r\nTo keep the values the same as those included in the credential offer, leave the body blank.", - "operationId": "IssueCredential", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "parameters": [ - { - "name": "credentialId", - "in": "path", - "description": "The credential identifier", - "required": true, - "type": "string" - }, - { - "in": "body", - "name": "body", - "description": "Updated credential values", - "required": false, - "schema": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - }, - "delete": { - "tags": [ - "Credentials" - ], - "summary": "Revoke an issued credential", - "description": "Revoke credential that was issued previously.\r\nProcess of revocation will update the revocation registry locally and on the ledger.\r\nIssued credentials can still participate in proof workflows and be considered valid, but only if the verifying ignores the revocation trail.", - "operationId": "RevokeCredential", - "parameters": [ - { - "name": "credentialId", - "in": "path", - "description": "The credential identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/credentials/delete/{credentialId}": { - "delete": { - "tags": [ - "Credentials" - ], - "summary": "Delete credential", - "description": "Delete the specified credential.\r\nThis endpoint does not revoke the credential.", - "operationId": "DeleteCredential", - "parameters": [ - { - "name": "credentialId", - "in": "path", - "description": "The credential identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/verificationPolicies": { - "get": { - "tags": [ - "Verification Policies" - ], - "summary": "List all verification policies", - "description": "List all verification policies for the authenticated organization.", - "operationId": "ListVerificationPolicies", - "produces": [ - "application/json" - ], - "parameters": [], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/VerificationPolicyContract" - } - } - } - } - }, - "post": { - "tags": [ - "Verification Policies" - ], - "summary": "Create verification policy", - "description": "Create a verification policy from the specified parameters.", - "operationId": "CreateVerificationPolicy", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "The verification policy parameters", - "required": true, - "schema": { - "$ref": "#/definitions/VerificationPolicyParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationPolicyContract" - } - } - } - } - }, - "/verificationPolicies/{policyId}": { - "get": { - "tags": [ - "Verification Policies" - ], - "summary": "Get verification policy", - "description": "Get the specified verification policy.", - "operationId": "GetVerificationPolicy", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "policyId", - "in": "path", - "description": "The verification policy identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationPolicyContract" - } - } - } - }, - "delete": { - "tags": [ - "Verification Policies" - ], - "summary": "Delete verification policy", - "description": "Delete the specified verification policy.", - "operationId": "DeleteVerificationPolicy", - "parameters": [ - { - "name": "policyId", - "in": "path", - "description": "The verification policy identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/definitions/credentials": { - "get": { - "tags": [ - "Definitions" - ], - "summary": "List all credential definitions", - "description": "List all credential definitions for the authorization context.", - "operationId": "ListCredentialDefinitions", - "produces": [ - "application/json" - ], - "parameters": [], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/CredentialDefinitionContract" - } - } - } - } - }, - "post": { - "tags": [ - "Definitions" - ], - "summary": "Create credential definition and schema from parameters", - "description": "A credential definition is created and saved on your cloud agent.", - "operationId": "CreateCredentialDefinition", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Definition parameters", - "required": true, - "schema": { - "$ref": "#/definitions/CredentialDefinitionFromSchemaParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/CredentialDefinitionContract" - } - } - } - } - }, - "/definitions/credentials/{definitionId}": { - "get": { - "tags": [ - "Definitions" - ], - "summary": "Get credential definition", - "description": "Get the specified credential definition.", - "operationId": "GetCredentialDefinition", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "definitionId", - "in": "path", - "description": "The credential definition identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/CredentialDefinitionContract" - } - } - } - } - }, - "/definitions/credentials/{schemaId}": { - "post": { - "tags": [ - "Definitions" - ], - "summary": "Create credential definition from schema", - "description": "Create a credential definition from the specified schema.", - "operationId": "CreateCredentialDefinitionForSchemaId", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "schemaId", - "in": "path", - "description": "The schema identifier", - "required": true, - "type": "string" - }, - { - "in": "body", - "name": "body", - "description": "The definition parameters", - "required": true, - "schema": { - "$ref": "#/definitions/CredentialDefinitionParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/CredentialDefinitionContract" - } - } - } - } - }, - "/definitions/credentials/{credentialDefinitionId}": { - "delete": { - "tags": [ - "Definitions" - ], - "summary": "Delete credential definition", - "description": "Delete the specified credential definition.", - "operationId": "DeleteCredentialDefinition", - "parameters": [ - { - "name": "credentialDefinitionId", - "in": "path", - "description": "The credential definition identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/definitions/schemas": { - "get": { - "tags": [ - "Definitions" - ], - "summary": "List all schemas", - "description": "List all schemas registered to or used by the authenticated organization.", - "operationId": "ListSchemas", - "produces": [ - "application/json" - ], - "parameters": [], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/SchemaRecord" - } - } - } - } - }, - "post": { - "tags": [ - "Definitions" - ], - "summary": "Create schema", - "description": "Register schema with the current agency tenant and write the schema to the ledger using the tenant as issuer.\r\nThis does not create credential definition.", - "operationId": "CreateSchema", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Schema parameters", - "required": true, - "schema": { - "$ref": "#/definitions/SchemaParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "string" - } - } - } - } - }, - "/definitions/verifications/{definitionId}": { - "get": { - "tags": [ - "Definitions" - ], - "summary": "[Deprecated] Get verification definition", - "description": "Please use Verification Policies endpoints.\r\nGet the specified verification definition.", - "operationId": "GetVerificationDefinition", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "definitionId", - "in": "path", - "description": "The verification definition identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationDefinitionContract" - } - } - } - } - }, - "/definitions/verifications": { - "get": { - "tags": [ - "Definitions" - ], - "summary": "[Deprecated] List all verification definitions", - "description": "Please use Verification Policies endpoints.\r\nList all verification definitions for the authenticated organization.", - "operationId": "ListVerificationDefinitions", - "produces": [ - "application/json" - ], - "parameters": [], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/VerificationDefinitionContract" - } - } - } - } - }, - "post": { - "tags": [ - "Definitions" - ], - "summary": "[Deprecated] Create verification definition", - "description": "Please use Verification Policies endpoints.\r\nA verification definition is created and saved on your cloud agent.\r\nYou can discover your definition based on the ID that is returned or the name.", - "operationId": "CreateVerificationDefinition", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "The proof request", - "required": true, - "schema": { - "$ref": "#/definitions/ProofRequest" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationDefinitionContract" - } - } - } - } - }, - "/definitions/verifications/{verificationDefinitionId}": { - "delete": { - "tags": [ - "Definitions" - ], - "summary": "[Deprecated] Delete verification definition", - "description": "Please use Verification Policies endpoints.\r\nDelete the specified verification definition.", - "operationId": "DeleteVerificationDefinition", - "parameters": [ - { - "name": "verificationDefinitionId", - "in": "path", - "description": "The verification definition identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/messages": { - "post": { - "tags": [ - "Messaging" - ], - "summary": "Send message", - "description": "Send a message from the specified message parameters.", - "operationId": "SendMessage", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "The message parameters", - "required": true, - "schema": { - "$ref": "#/definitions/BasicMessageParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/messages/connection/{connectionId}": { - "get": { - "tags": [ - "Messaging" - ], - "summary": "List all messages for connection", - "description": "List all messages for the specified connection.", - "operationId": "ListMessages", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "connectionId", - "in": "path", - "description": "The connection identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/BasicMessageRecord" - } - } - } - } - } - }, - "/messages/{messageId}": { - "get": { - "tags": [ - "Messaging" - ], - "summary": "Get message", - "description": "Get the details for the specified message.", - "operationId": "GetMessage", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "messageId", - "in": "path", - "description": "The message identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/BasicMessageContract" - } - } - } - } - }, - "/tenants": { - "get": { - "tags": [ - "Tenants" - ], - "summary": "[Deprecated] List all tenants", - "description": "Please use the Provider API instead.\r\nList all tenants for the current authorization context.", - "operationId": "ListTenants", - "produces": [ - "application/json" - ], - "parameters": [], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/TenantContract" - } - } - } - } - }, - "post": { - "tags": [ - "Tenants" - ], - "summary": "[Deprecated] Create tenant", - "description": "Please use the Provider API instead.\r\nCreate a new tenant and setup a unique agency endpoint.\r\nThe agency will be set as an issuer.", - "operationId": "CreateTenant", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "Configuration options for creating new tenant", - "required": true, - "schema": { - "$ref": "#/definitions/TenantParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/TenantContract" - } - } - } - } - }, - "/tenants/{tenantId}": { - "get": { - "tags": [ - "Tenants" - ], - "summary": "[Deprectaed] Get tenant", - "description": "Please use the Provider API instead.\r\nGet the configuration for the specified tenant.", - "operationId": "GetTenant", - "produces": [ - "application/json" - ], - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "The tenant identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/TenantContract" - } - } - } - }, - "delete": { - "tags": [ - "Tenants" - ], - "summary": "[Deprecated] Delete tenant", - "description": "Please use the Provider API instead.\r\nPermanently remove the specified tenant, including their wallet, endpoint registrations and all data.\r\nAll definitions, connections and credentials issued will be deleted.\r\nThis action cannot be reverted.", - "operationId": "DeleteTenant", - "parameters": [ - { - "name": "tenantId", - "in": "path", - "description": "The tenant identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/tenants/issuerStatus": { - "get": { - "tags": [ - "Tenants" - ], - "summary": "[Deprecated] Get issuer status for current tenant", - "description": "Please use the Provider API instead.\r\nIf the tenant is configured with Dedicated endorsement, this action will check if the issuer DID has the required ENDORSER role on the configured ledger network.\r\nAdditionally, check the acceptance of the transaction author agreement and return the text and version if acceptance is required.", - "operationId": "GetIssuerStatus", - "produces": [ - "application/json" - ], - "parameters": [], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/IssuerStatusContract" - } - } - } - } - }, - "/verifications/policy/{policyId}/connections/{connectionId}": { - "put": { - "tags": [ - "Verifications" - ], - "summary": "Send verification to connection from policy", - "description": "Send a verification to the specified connection using an existing policy.", - "operationId": "SendVerificationFromPolicy", - "produces": [ - "text/plain", - "application/json", - "text/json" - ], - "parameters": [ - { - "name": "connectionId", - "in": "path", - "description": "The connection identifier", - "required": true, - "type": "string" - }, - { - "name": "policyId", - "in": "path", - "description": "The policy identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationContract" - } - } - } - } - }, - "/verifications/policy/connections/{connectionId}": { - "post": { - "tags": [ - "Verifications" - ], - "summary": "Send verification to connection from parameters", - "description": "Send a verification request to the specified connection from a set of parameters.", - "operationId": "SendVerificationFromParameters", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "text/plain", - "application/json", - "text/json" - ], - "parameters": [ - { - "name": "connectionId", - "in": "path", - "description": "The connection identifier", - "required": true, - "type": "string" - }, - { - "in": "body", - "name": "body", - "description": "The policy parameters", - "required": true, - "schema": { - "$ref": "#/definitions/VerificationPolicyParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationContract" - } - } - } - } - }, - "/verifications/proposal/{policyId}/connections/{connectionId}": { - "put": { - "tags": [ - "Verifications" - ], - "summary": "Send verification from policy", - "description": "Create a verification from the specified policy and send it to the specified connection.", - "operationId": "SendVerificationFromProposal", - "produces": [ - "text/plain", - "application/json", - "text/json" - ], - "parameters": [ - { - "name": "connectionId", - "in": "path", - "description": "The connection identifier", - "required": true, - "type": "string" - }, - { - "name": "policyId", - "in": "path", - "description": "The verification policy identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationContract" - } - } - } - } - }, - "/verifications/policy/{policyId}": { - "put": { - "tags": [ - "Verifications" - ], - "summary": "Create connectionless verification from policy", - "description": "Create a connectionless verification from the specified policy.\r\nConnectionless transport uses URLs that can be shared with the user over any existing transport (email, SMS, web).", - "operationId": "CreateVerificationFromPolicy", - "produces": [ - "text/plain", - "application/json", - "text/json" - ], - "parameters": [ - { - "name": "policyId", - "in": "path", - "description": "The policy identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationContract" - } - } - } - } - }, - "/verifications/policy": { - "post": { - "tags": [ - "Verifications" - ], - "summary": "Create connectionless verification from parameters", - "description": "Create verification from parameters.\r\nConnectionless transport uses URLs that can be shared with the user over any existing transport (email, SMS, web).", - "operationId": "CreateVerificationFromParameters", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "text/plain", - "application/json", - "text/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "The policy parameters", - "required": true, - "schema": { - "$ref": "#/definitions/VerificationPolicyParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationContract" - } - } - } - } - }, - "/verifications": { - "get": { - "tags": [ - "Verifications" - ], - "summary": "List all verifications", - "description": "List all verifications.\r\nOptionally filter by connection and/or definition.", - "operationId": "ListVerifications", - "produces": [ - "text/plain", - "application/json", - "text/json" - ], - "parameters": [ - { - "name": "connectionId", - "in": "query", - "description": "The connection identifier", - "required": false, - "type": "string" - }, - { - "name": "definitionId", - "in": "query", - "description": "The definition identifier", - "required": false, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/VerificationContract" - } - } - } - } - }, - "post": { - "tags": [ - "Verifications" - ], - "summary": "[Deprecated] Create/send verification", - "description": "Please use SendVerification(policyId) or CreateVerification(policyId) instead.\r\nThis endpoint can be used to send a verification definition to a connection, which will create a verification ID to track the response from the connection.\r\nIf the parameter {connectionId} is not specified, this endpoint will create a connectionless verification. A URL will be generated that can be shared with anonymous user.", - "operationId": "CreateVerification", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "text/plain", - "application/json", - "text/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "The verification parameters", - "required": true, - "schema": { - "$ref": "#/definitions/VerificationParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationContract" - } - } - } - } - }, - "/verifications/{verificationId}": { - "get": { - "tags": [ - "Verifications" - ], - "summary": "Get verification", - "description": "Get the specified verification.", - "operationId": "GetVerification", - "produces": [ - "text/plain", - "application/json", - "text/json" - ], - "parameters": [ - { - "name": "verificationId", - "in": "path", - "description": "The verification identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationContract" - } - } - } - }, - "delete": { - "tags": [ - "Verifications" - ], - "summary": "Delete verification", - "description": "Delete the specified verification.", - "operationId": "DeleteVerification", - "parameters": [ - { - "name": "verificationId", - "in": "path", - "description": "The verification identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/verifications/{verificationId}/verify": { - "get": { - "tags": [ - "Verifications" - ], - "summary": "[Deprecated] Verify verification", - "description": "This action is now obsolete. Verifications are automatically verified when they are received.\r\nExecute verification on this record. This is an expensive action and is executed by verifying the proof against the ledger data.", - "operationId": "VerifyVerification", - "produces": [ - "text/plain", - "application/json", - "text/json" - ], - "parameters": [ - { - "name": "verificationId", - "in": "path", - "description": "The verification identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "deprecated": true, - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/VerificationResult" - } - } - } - } - }, - "/webhooks": { - "get": { - "tags": [ - "Webhooks" - ], - "summary": "List all webhooks", - "description": "List all webhooks registered with the authenticated organization.", - "operationId": "ListWebhooks", - "produces": [ - "application/json" - ], - "parameters": [], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "type": "array", - "items": { - "$ref": "#/definitions/WebhookContract" - } - } - } - } - }, - "post": { - "tags": [ - "Webhooks" - ], - "summary": "Create webhook", - "description": "Register a webhook with the authenticated organization.", - "operationId": "CreateWebhook", - "consumes": [ - "application/json", - "text/json", - "application/*+json" - ], - "produces": [ - "application/json" - ], - "parameters": [ - { - "in": "body", - "name": "body", - "description": "The webhook parameters", - "required": true, - "schema": { - "$ref": "#/definitions/WebhookParameters" - } - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success", - "schema": { - "$ref": "#/definitions/WebhookContract" - } - } - } - } - }, - "/webhooks/{webhookId}": { - "delete": { - "tags": [ - "Webhooks" - ], - "summary": "Remove webhook", - "description": "Remove a registered webhook from the authenticated organization.", - "operationId": "RemoveWebhook", - "parameters": [ - { - "name": "webhookId", - "in": "path", - "description": "The webhook identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/webhooks/{webhookId}/enable": { - "put": { - "tags": [ - "Webhooks" - ], - "summary": "Enable webhook", - "description": "Enable a registered webhook for the authenticated organization.", - "operationId": "EnableWebhook", - "parameters": [ - { - "name": "webhookId", - "in": "path", - "description": "The webhook identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/webhooks/{webhookId}/disable": { - "put": { - "tags": [ - "Webhooks" - ], - "summary": "Disable webhook", - "description": "Enable a registered webhook for the authenticated organization.", - "operationId": "DisableWebhook", - "parameters": [ - { - "name": "webhookId", - "in": "path", - "description": "The webhook identifier", - "required": true, - "type": "string" - } - ], - "security": [ - { - "oauth2": [] - } - ], - "responses": { - "200": { - "description": "Success" - } - } - } - }, - "/health": { - "get": { - "tags": [ - "Diagnostics" - ], - "summary": "Health check", - "description": "Check the health of the API.", - "operationId": "Health", - "parameters": [], - "responses": { - "200": { - "description": "Success" - } - } - } - } - }, - "securityDefinitions": { - "oauth2": { - "description": "Standard Authorization header using the Bearer scheme and an Access Token. Example: \"Bearer {Access Token}\"", - "type": "apiKey", - "name": "Authorization", - "in": "header" - } - }, - "definitions": { - "NetworkContract": { - "type": "object", - "properties": { - "networkId": { - "type": "string" - }, - "networkName": { - "type": "string" - }, - "genesisTransactions": { - "type": "string" - }, - "poolProtocolVersion": { - "type": "integer", - "format": "int32" - } - } - }, - "NetworkTxnAgreementContract": { - "type": "object", - "properties": { - "text": { - "type": "string", - "description": "Acceptance agreement text" - }, - "version": { - "type": "string", - "description": "Agreement version" - }, - "acceptanceMethods": { - "type": "object", - "description": "List of agreement acceptance methods", - "additionalProperties": { - "type": "string" - } - } - }, - "description": "Transaction author agreement" - }, - "ConnectionState": { - "type": "string", - "enum": [ - "Invited", - "Negotiating", - "Connected" - ] - }, - "AgentEndpoint": { - "type": "object", - "properties": { - "did": { - "type": "string", - "readOnly": true - }, - "verkey": { - "type": "array", - "readOnly": true, - "items": { - "type": "string" - } - }, - "uri": { - "type": "string", - "readOnly": true - } - } - }, - "ConnectionContract": { - "type": "object", - "properties": { - "connectionId": { - "type": "string" - }, - "name": { - "type": "string" - }, - "imageUrl": { - "type": "string" - }, - "myDid": { - "type": "string" - }, - "theirDid": { - "type": "string" - }, - "myKey": { - "type": "string" - }, - "theirKey": { - "type": "string" - }, - "state": { - "$ref": "#/definitions/ConnectionState" - }, - "invitation": { - "type": "string" - }, - "invitationUrl": { - "type": "string" - }, - "endpoint": { - "$ref": "#/definitions/AgentEndpoint" - }, - "createdAtUtc": { - "type": "string", - "format": "date-time" - }, - "multiParty": { - "type": "boolean" - } - } - }, - "ConnectionInvitationParameters": { - "type": "object", - "properties": { - "connectionId": { - "type": "string", - "description": "Unique connection identifier. If not specified, a random one will be generated." - }, - "multiParty": { - "type": "boolean", - "description": "If set to 'true', the invitation can be used by multiple parties and will always have the status set to 'Invited'.\r\nWhen a party accepts this invitation, a new connection record with a unique identifier will be created.\r\nDefault value is 'false'." - }, - "name": { - "type": "string", - "description": "Name that can be used as an alias for the connection.\r\nDefault value is 'null'." - } - }, - "description": "Connection invitation parameters" - }, - "CredentialState": { - "type": "string", - "enum": [ - "Offered", - "Requested", - "Issued", - "Rejected", - "Revoked" - ] - }, - "CredentialContract": { - "type": "object", - "properties": { - "credentialId": { - "type": "string" - }, - "state": { - "$ref": "#/definitions/CredentialState" - }, - "connectionId": { - "type": "string" - }, - "definitionId": { - "type": "string" - }, - "schemaId": { - "type": "string" - }, - "offerData": { - "type": "string" - }, - "offerUrl": { - "type": "string" - }, - "issuedAtUtc": { - "type": "string", - "format": "date-time" - }, - "acceptedAtUtc": { - "type": "string", - "format": "date-time" - }, - "values": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "correlationId": { - "type": "string" - } - } - }, - "CredentialOfferParameters": { - "type": "object", - "required": [ - "definitionId" - ], - "properties": { - "definitionId": { - "type": "string", - "description": "The credential definition identifier" - }, - "connectionId": { - "type": "string", - "description": "Connection identifier to send this credential to.\r\nIf omitted, the request will be treated as connectionless issuance and will generate a URL." - }, - "automaticIssuance": { - "type": "boolean", - "description": "If true, the credential will automatically be issued once the individual accepts the offer.\r\nIf false, when an individual accepts the offer the credential will be in state 'Requested' and must be manually issued.\r\nDefault value is 'false'." - }, - "credentialValues": { - "type": "object", - "additionalProperties": { - "type": "string" - } - } - }, - "description": "Send offer." - }, - "VerificationPolicyRestrictionAttribute": { - "type": "object", - "properties": { - "attributeName": { - "type": "string" - }, - "attributeValue": { - "type": "string" - } - } - }, - "VerificationPolicyRestriction": { - "type": "object", - "properties": { - "schemaId": { - "type": "string" - }, - "schemaIssuerDid": { - "type": "string" - }, - "schemaName": { - "type": "string" - }, - "schemaVersion": { - "type": "string" - }, - "issuerDid": { - "type": "string" - }, - "credentialDefinitionId": { - "type": "string" - }, - "value": { - "$ref": "#/definitions/VerificationPolicyRestrictionAttribute" - } - } - }, - "VerificationPolicyAttributeContract": { - "type": "object", - "required": [ - "attributeNames", - "policyName" - ], - "properties": { - "policyName": { - "type": "string" - }, - "attributeNames": { - "type": "array", - "items": { - "type": "string" - } - }, - "restrictions": { - "type": "array", - "items": { - "$ref": "#/definitions/VerificationPolicyRestriction" - } - } - } - }, - "VerificationPolicyPredicateContract": { - "type": "object", - "required": [ - "attributeName", - "policyName", - "predicateType", - "predicateValue" - ], - "properties": { - "policyName": { - "type": "string" - }, - "attributeName": { - "type": "string" - }, - "predicateType": { - "type": "string" - }, - "predicateValue": { - "type": "integer", - "format": "int32" - }, - "restrictions": { - "type": "array", - "items": { - "$ref": "#/definitions/VerificationPolicyRestriction" - } - } - } - }, - "VerificationPolicyRevocationRequirement": { - "type": "object", - "properties": { - "validAt": { - "type": "string", - "format": "date-time" - } - } - }, - "VerificationPolicyContract": { - "type": "object", - "required": [ - "name", - "version" - ], - "properties": { - "policyId": { - "type": "string" - }, - "name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "attributes": { - "type": "array", - "items": { - "$ref": "#/definitions/VerificationPolicyAttributeContract" - } - }, - "predicates": { - "type": "array", - "items": { - "$ref": "#/definitions/VerificationPolicyPredicateContract" - } - }, - "revocationRequirement": { - "$ref": "#/definitions/VerificationPolicyRevocationRequirement" - } - } - }, - "VerificationPolicyParameters": { - "type": "object", - "required": [ - "name", - "version" - ], - "properties": { - "name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "attributes": { - "type": "array", - "items": { - "$ref": "#/definitions/VerificationPolicyAttributeContract" - } - }, - "predicates": { - "type": "array", - "items": { - "$ref": "#/definitions/VerificationPolicyPredicateContract" - } - }, - "revocationRequirement": { - "$ref": "#/definitions/VerificationPolicyRevocationRequirement" - } - } - }, - "CredentialDefinitionContract": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "attributes": { - "type": "array", - "items": { - "type": "string" - } - }, - "supportsRevocation": { - "type": "boolean" - }, - "schemaId": { - "type": "string" - }, - "definitionId": { - "type": "string" - } - } - }, - "CredentialDefinitionFromSchemaParameters": { - "type": "object", - "required": [ - "attributes", - "name", - "version" - ], - "properties": { - "name": { - "type": "string", - "description": "Schema name" - }, - "version": { - "type": "string", - "description": "Schema version" - }, - "attributes": { - "type": "array", - "description": "Schema attribute names", - "items": { - "type": "string" - } - }, - "supportRevocation": { - "type": "boolean", - "description": "Support credential revocation" - }, - "tag": { - "type": "string", - "description": "Unique tag to differentiate definitions of the same schema" - } - }, - "description": "Represents a request object to create new credential definition for an agency" - }, - "CredentialDefinitionParameters": { - "type": "object", - "properties": { - "supportRevocation": { - "type": "boolean", - "description": "Support credential revocation" - }, - "tag": { - "type": "string", - "description": "Unique tag to differentiate definitions of the same schema" - } - }, - "description": "Credential definition parameters" - }, - "SchemaParameters": { - "type": "object", - "required": [ - "attributeNames", - "name", - "version" - ], - "properties": { - "name": { - "type": "string", - "description": "The schema name" - }, - "version": { - "type": "string", - "description": "The schema version" - }, - "attributeNames": { - "type": "array", - "description": "The attribute names", - "items": { - "type": "string" - } - } - }, - "description": "Schema." - }, - "SchemaRecord": { - "type": "object", - "properties": { - "typeName": { - "type": "string", - "readOnly": true - }, - "name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "attributeNames": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "createdAtUtc": { - "type": "string", - "format": "date-time", - "readOnly": true - }, - "updatedAtUtc": { - "type": "string", - "format": "date-time", - "readOnly": true - } - } - }, - "AttributeValue": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - } - } - }, - "AttributeFilter": { - "type": "object", - "properties": { - "schemaId": { - "type": "string" - }, - "schemaIssuerDid": { - "type": "string" - }, - "schemaName": { - "type": "string" - }, - "schemaVersion": { - "type": "string" - }, - "issuerDid": { - "type": "string" - }, - "credentialDefinitionId": { - "type": "string" - }, - "attributeValue": { - "$ref": "#/definitions/AttributeValue" - } - } - }, - "RevocationInterval": { - "type": "object", - "properties": { - "from": { - "type": "integer", - "format": "int32" - }, - "to": { - "type": "integer", - "format": "int32" - } - } - }, - "ProofAttributeInfo": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "names": { - "type": "array", - "items": { - "type": "string" - } - }, - "restrictions": { - "type": "array", - "items": { - "$ref": "#/definitions/AttributeFilter" - } - }, - "nonRevoked": { - "$ref": "#/definitions/RevocationInterval" - } - } - }, - "ProofPredicateInfo": { - "type": "object", - "properties": { - "predicateType": { - "type": "string" - }, - "predicateValue": { - "type": "integer", - "format": "int32" - }, - "name": { - "type": "string" - }, - "names": { - "type": "array", - "items": { - "type": "string" - } - }, - "restrictions": { - "type": "array", - "items": { - "$ref": "#/definitions/AttributeFilter" - } - }, - "nonRevoked": { - "$ref": "#/definitions/RevocationInterval" - } - } - }, - "ProofRequest": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "nonce": { - "type": "string" - }, - "requestedAttributes": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ProofAttributeInfo" - } - }, - "requestedPredicates": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ProofPredicateInfo" - } - }, - "nonRevoked": { - "$ref": "#/definitions/RevocationInterval" - } - } - }, - "VerificationDefinitionContract": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "The verification identifier" - }, - "proofRequest": { - "$ref": "#/definitions/ProofRequest" - } - }, - "description": "" - }, - "BasicMessageParameters": { - "type": "object", - "properties": { - "connectionId": { - "type": "string", - "description": "The connection identifier" - }, - "text": { - "type": "string", - "description": "The message text" - } - }, - "description": "Basic message parameters" - }, - "MessageDirection": { - "type": "string", - "enum": [ - "Incoming", - "Outgoing" - ] - }, - "BasicMessageRecord": { - "type": "object", - "properties": { - "typeName": { - "type": "string", - "readOnly": true - }, - "connectionId": { - "type": "string" - }, - "sentTime": { - "type": "string", - "format": "date-time" - }, - "direction": { - "$ref": "#/definitions/MessageDirection" - }, - "text": { - "type": "string" - }, - "id": { - "type": "string" - }, - "createdAtUtc": { - "type": "string", - "format": "date-time", - "readOnly": true - }, - "updatedAtUtc": { - "type": "string", - "format": "date-time", - "readOnly": true - } - } - }, - "BasicMessageContract": { - "type": "object", - "properties": { - "connectionId": { - "type": "string", - "description": "The connection identifier" - }, - "timestamp": { - "type": "string", - "format": "date-time", - "description": "The message timestamp" - }, - "text": { - "type": "string", - "description": "The message text" - }, - "direction": { - "$ref": "#/definitions/MessageDirection" - } - }, - "description": "Basic message contract" - }, - "EndorserType": { - "type": "string", - "enum": [ - "Shared", - "Dedicated", - "Delegated" - ] - }, - "TenantExtendedInformationContract": { - "type": "object", - "properties": { - "issuerDid": { - "type": "string", - "description": "Issuer DID" - }, - "issuerKey": { - "type": "string", - "description": "Issuer Public Key" - }, - "issuerKeyGenerationSeed": { - "type": "string", - "description": "Issuer key generation seed used for deterministic key creation (32 characters)" - }, - "agentDid": { - "type": "string", - "description": "Agent DID" - }, - "agentKey": { - "type": "string", - "description": "Agent Public Key" - }, - "agentKeyGenerationSeed": { - "type": "string", - "description": "Agent key generation seed used for deterministic key creation (32 characters)" - }, - "agentServiceEndpoint": { - "type": "string", - "description": "Agent service endpoint URL" - }, - "transactionEndorsement": { - "$ref": "#/definitions/EndorserType" - } - }, - "description": "Extended tenant information" - }, - "TenantContract": { - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "The tenant name", - "readOnly": true - }, - "imageUrl": { - "type": "string", - "description": "The image URL" - }, - "network": { - "$ref": "#/definitions/NetworkContract" - }, - "tenantId": { - "type": "string", - "description": "The tenant identifier", - "readOnly": true - }, - "extendedInformation": { - "$ref": "#/definitions/TenantExtendedInformationContract" - } - }, - "description": "Tenant info" - }, - "TenantParameters": { - "type": "object", - "required": [ - "name" - ], - "properties": { - "issuerSeed": { - "type": "string", - "description": "Issuer seed used for deterministic DID generation.\r\nIf omitted, a random DID/Key is generated." - }, - "name": { - "type": "string", - "description": "The tenant name" - }, - "imageUrl": { - "type": "string", - "description": "URL of tenant profile image" - }, - "networkId": { - "type": "string", - "description": "Ledger network identifier.\r\nDefault is Sovrin Staging (sovrin-staging)." - }, - "endorserType": { - "$ref": "#/definitions/EndorserType" - } - }, - "description": "Configuration options for creating new tenant" - }, - "IssuerStatusContract": { - "type": "object", - "properties": { - "acceptanceText": { - "type": "string", - "description": "Transaction Author Agreement Text" - }, - "acceptanceVersion": { - "type": "string", - "description": "Transaction Author Agreement version" - }, - "acceptanceDigest": { - "type": "string", - "description": "The acceptance digest" - }, - "acceptanceTime": { - "type": "integer", - "format": "int64", - "description": "The acceptance time" - }, - "requireAcceptance": { - "type": "boolean", - "description": "Indicates if user needs to accept the latest agreement on the network" - }, - "transactionEndorsement": { - "$ref": "#/definitions/EndorserType" - }, - "issuerCanEndorse": { - "type": "boolean", - "description": "Indicates if the user has Endorser status" - } - }, - "description": "Issuer Status contract" - }, - "ProofState": { - "type": "string", - "enum": [ - "Proposed", - "Requested", - "Accepted", - "Rejected" - ] - }, - "ProofAttributeContract": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "value": { - "type": "string" - }, - "attributes": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "revealed": { - "type": "boolean", - "readOnly": true - }, - "selfAttested": { - "type": "boolean" - }, - "conditional": { - "type": "boolean" - } - } - }, - "ProposedAttribute": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "credentialDefinitionId": { - "type": "string" - }, - "schemaId": { - "type": "string" - }, - "issuerDid": { - "type": "string" - }, - "mimeType": { - "type": "string" - }, - "value": { - "type": "string" - }, - "referent": { - "type": "string" - } - } - }, - "ProposedPredicate": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "credentialDefinitionId": { - "type": "string" - }, - "issuerDid": { - "type": "string" - }, - "schemaId": { - "type": "string" - }, - "predicate": { - "type": "string" - }, - "threshold": { - "type": "integer", - "format": "int32" - }, - "referent": { - "type": "string" - } - } - }, - "ProofProposal": { - "type": "object", - "properties": { - "comment": { - "type": "string" - }, - "proposedAttributes": { - "type": "array", - "items": { - "$ref": "#/definitions/ProposedAttribute" - } - }, - "proposedPredicates": { - "type": "array", - "items": { - "$ref": "#/definitions/ProposedPredicate" - } - } - } - }, - "VerificationContract": { - "type": "object", - "properties": { - "connectionId": { - "type": "string" - }, - "verificationId": { - "type": "string" - }, - "definitionId": { - "type": "string" - }, - "state": { - "$ref": "#/definitions/ProofState" - }, - "createdAtUtc": { - "type": "string", - "format": "date-time" - }, - "updatedAtUtc": { - "type": "string", - "format": "date-time" - }, - "isValid": { - "type": "boolean" - }, - "verifiedAtUtc": { - "type": "string", - "format": "date-time" - }, - "proof": { - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ProofAttributeContract" - } - }, - "policy": { - "$ref": "#/definitions/VerificationPolicyParameters" - }, - "proposal": { - "$ref": "#/definitions/ProofProposal" - }, - "verificationRequestData": { - "type": "string" - }, - "verificationRequestUrl": { - "type": "string" - } - } - }, - "VerificationParameters": { - "type": "object", - "required": [ - "verificationDefinitionId" - ], - "properties": { - "verificationDefinitionId": { - "type": "string", - "description": "Verification definition identifier" - }, - "connectionId": { - "type": "string", - "description": "Connection identifier" - } - }, - "description": "Create verification" - }, - "VerificationResult": { - "type": "object", - "properties": { - "valid": { - "type": "boolean", - "description": "True if verification passed, otherwise False" - }, - "proof": { - "type": "object", - "description": "Verification Proof Details", - "additionalProperties": { - "$ref": "#/definitions/ProofAttributeContract" - } - } - }, - "description": "Verification result" - }, - "WebhookType": { - "type": "string", - "enum": [ - "Notification", - "DelegatedEndorser", - "Zapier" - ] - }, - "WebhookContract": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/WebhookType" - }, - "enabled": { - "type": "boolean" - }, - "id": { - "type": "string" - } - } - }, - "WebhookParameters": { - "type": "object", - "properties": { - "url": { - "type": "string" - }, - "type": { - "$ref": "#/definitions/WebhookType" - }, - "parameters": { - "type": "string" - } - } - } - } -} \ No newline at end of file diff --git a/infrastructure/local/run.sh b/infrastructure/local/run.sh index 3bc5e6987f..b9d016a8d8 100755 --- a/infrastructure/local/run.sh +++ b/infrastructure/local/run.sh @@ -125,5 +125,6 @@ fi PORT=${PORT} NETWORK=${NETWORK} DOCKERHOST=${DOCKERHOST} docker compose \ -p ${NAME} \ + ${DEBUG} \ -f ${SCRIPT_DIR}/../shared/docker-compose.yml \ --env-file ${ENV_FILE} ${DEBUG} up ${BACKGROUND} ${WAIT} 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 d81349731e..7e8b362eb3 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 @@ -55,26 +55,3 @@ class DidCommX() extends DidOps /* with DidAgent with DIDResolver */ { } yield (ret) } - -// object AgentService { -// val alice = ZLayer.succeed( -// AgentService[Agent.Alice.type]( -// new DIDComm(UniversalDidResolver, AliceSecretResolver.secretResolver), -// Agent.Alice -// ) -// ) -// val bob = ZLayer.succeed( -// AgentService[Agent.Bob.type]( -// new DIDComm(UniversalDidResolver, BobSecretResolver.secretResolver), -// Agent.Bob -// ) -// ) - -// // val charlie = ZLayer.succeed( -// // AgentService[Agent.Charlie.type]( -// // new DIDComm(UniversalDidResolver, CharlieSecretResolver.secretResolver), -// // Agent.Charlie -// // ) -// // ) - -// } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialConfiguration.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialConfiguration.scala new file mode 100644 index 0000000000..fe48297132 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialConfiguration.scala @@ -0,0 +1,20 @@ +package org.hyperledger.identus.pollux.core.model.oid4vci + +import org.hyperledger.identus.pollux.core.model.CredentialFormat + +import java.net.URI +import java.time.temporal.ChronoUnit +import java.time.Instant + +final case class CredentialConfiguration( + configurationId: String, + format: CredentialFormat, + schemaId: URI, + createdAt: Instant +) { + def scope: String = configurationId + + def withTruncatedTimestamp(unit: ChronoUnit = ChronoUnit.MICROS): CredentialConfiguration = copy( + createdAt = createdAt.truncatedTo(unit), + ) +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialIssuer.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialIssuer.scala new file mode 100644 index 0000000000..9392849f4f --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/oid4vci/CredentialIssuer.scala @@ -0,0 +1,30 @@ +package org.hyperledger.identus.pollux.core.model.oid4vci + +import java.net.URL +import java.time.temporal.ChronoUnit +import java.time.Instant +import java.util.UUID + +case class CredentialIssuer( + id: UUID, + authorizationServer: URL, + authorizationServerClientId: String, + authorizationServerClientSecret: String, + createdAt: Instant, + updatedAt: Instant +) { + def withTruncatedTimestamp(unit: ChronoUnit = ChronoUnit.MICROS): CredentialIssuer = copy( + createdAt = createdAt.truncatedTo(unit), + updatedAt = updatedAt.truncatedTo(unit), + ) +} + +object CredentialIssuer { + def apply(authorizationServer: URL, clientId: String, clientSecret: String): CredentialIssuer = + apply(UUID.randomUUID(), authorizationServer, clientId, clientSecret) + + def apply(id: UUID, authorizationServer: URL, clientId: String, clientSecret: String): CredentialIssuer = { + val now = Instant.now + CredentialIssuer(id, authorizationServer, clientId, clientSecret, now, now).withTruncatedTimestamp() + } +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala index 2ec8445a2b..2486c17cf2 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/model/schema/CredentialSchema.scala @@ -116,16 +116,22 @@ object CredentialSchema { given JsonEncoder[CredentialSchema] = DeriveJsonEncoder.gen[CredentialSchema] given JsonDecoder[CredentialSchema] = DeriveJsonDecoder.gen[CredentialSchema] + def resolveJWTSchema(uri: URI, uriDereferencer: URIDereferencer): IO[CredentialSchemaParsingError, Json] = { + for { + content <- uriDereferencer.dereference(uri).orDieAsUnmanagedFailure + json <- ZIO + .fromEither(content.fromJson[Json]) + .mapError(error => CredentialSchemaParsingError(error)) + } yield json + } + def validSchemaValidator( schemaId: String, uriDereferencer: URIDereferencer ): IO[InvalidURI | CredentialSchemaParsingError, JsonSchemaValidator] = { for { uri <- ZIO.attempt(new URI(schemaId)).mapError(_ => InvalidURI(schemaId)) - content <- uriDereferencer.dereference(uri).orDieAsUnmanagedFailure - json <- ZIO - .fromEither(content.fromJson[Json]) - .mapError(error => CredentialSchemaParsingError(error)) + json <- resolveJWTSchema(uri, uriDereferencer) schemaValidator <- JsonSchemaValidatorImpl .from(json) .orElse( diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/OID4VCIIssuerMetadataRepository.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/OID4VCIIssuerMetadataRepository.scala new file mode 100644 index 0000000000..a74d6641a7 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/OID4VCIIssuerMetadataRepository.scala @@ -0,0 +1,28 @@ +package org.hyperledger.identus.pollux.core.repository + +import org.hyperledger.identus.pollux.core.model.oid4vci.{CredentialConfiguration, CredentialIssuer} +import org.hyperledger.identus.shared.models.WalletAccessContext +import zio.* + +import java.net.URL +import java.util.UUID + +trait OID4VCIIssuerMetadataRepository { + def findIssuerById(issuerId: UUID): UIO[Option[CredentialIssuer]] + def createIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, Unit] + def findWalletIssuers: URIO[WalletAccessContext, Seq[CredentialIssuer]] + def updateIssuer( + issuerId: UUID, + authorizationServer: Option[URL] = None, + authorizationServerClientId: Option[String] = None, + authorizationServerClientSecret: Option[String] = None + ): URIO[WalletAccessContext, Unit] + def deleteIssuer(issuerId: UUID): URIO[WalletAccessContext, Unit] + def createCredentialConfiguration(issuerId: UUID, config: CredentialConfiguration): URIO[WalletAccessContext, Unit] + def findCredentialConfigurationsByIssuer(issuerId: UUID): UIO[Seq[CredentialConfiguration]] + def findCredentialConfigurationById( + issuerId: UUID, + configurationId: String + ): URIO[WalletAccessContext, Option[CredentialConfiguration]] + def deleteCredentialConfiguration(issuerId: UUID, configurationId: String): URIO[WalletAccessContext, Unit] +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala index 6f784bad9c..f934e1ddbb 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/repository/PresentationRepositoryInMemory.scala @@ -143,14 +143,14 @@ class PresentationRepositoryInMemory( sdJwtClaimsToDisclose: Option[SdJwtCredentialToDisclose], protocolState: ProtocolState ): URIO[WalletAccessContext, Unit] = { - val result: URIO[WalletAccessContext, Unit] = { + val result = { for { storeRef <- walletStoreRef maybeRecord <- findPresentationRecord(recordId) result <- maybeRecord .map(record => for { - result <- storeRef.update(r => + _ <- storeRef.update(r => r.updated( recordId, record.copy( @@ -163,7 +163,7 @@ class PresentationRepositoryInMemory( ) ) ) - } yield result + } yield 1 ) .getOrElse(ZIO.succeed(0)) } yield result diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala index 910f932dd4..97a0e10119 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialService.scala @@ -3,6 +3,7 @@ package org.hyperledger.identus.pollux.core.service import io.circe.{Json, JsonObject} import io.circe.syntax.* import org.hyperledger.identus.castor.core.model.did.CanonicalPrismDID +import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID, VerificationRelationship} import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.issuecredential.{ Attribute, @@ -13,6 +14,7 @@ import org.hyperledger.identus.mercury.protocol.issuecredential.{ import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError.* +import org.hyperledger.identus.pollux.vc.jwt.Issuer import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{Duration, IO, UIO, URIO, ZIO} @@ -150,6 +152,10 @@ trait CredentialService { failReason: Option[String] ): URIO[WalletAccessContext, Unit] + def getJwtIssuer( + jwtIssuerDID: PrismDID, + verificationRelationship: VerificationRelationship + ): URIO[WalletAccessContext, Issuer] } object CredentialService { 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 3ac0291ff8..fafb8073fc 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 @@ -72,7 +72,7 @@ object CredentialServiceImpl { private val VC_JSON_SCHEMA_TYPE = "CredentialSchema2022" } -private class CredentialServiceImpl( +class CredentialServiceImpl( credentialRepository: CredentialRepository, credentialStatusListRepository: CredentialStatusListRepository, didResolver: DidResolver, @@ -503,7 +503,7 @@ private class CredentialServiceImpl( } yield keyId } - private def getJwtIssuer( + override def getJwtIssuer( jwtIssuerDID: PrismDID, verificationRelationship: VerificationRelationship ): URIO[WalletAccessContext, JwtIssuer] = { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala index 7bbcf5f2ff..38418d08d0 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceNotifier.scala @@ -1,13 +1,14 @@ package org.hyperledger.identus.pollux.core.service import io.circe.Json -import org.hyperledger.identus.castor.core.model.did.CanonicalPrismDID +import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID, VerificationRelationship} import org.hyperledger.identus.event.notification.* import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.issuecredential.{IssueCredential, OfferCredential, RequestCredential} import org.hyperledger.identus.pollux.core.model.{DidCommID, IssueCredentialRecord} import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError.* +import org.hyperledger.identus.pollux.vc.jwt.Issuer import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{Duration, UIO, URIO, URLayer, ZIO, ZLayer} @@ -221,6 +222,12 @@ class CredentialServiceNotifier( states: IssueCredentialRecord.ProtocolState* ): UIO[Seq[IssueCredentialRecord]] = svc.getIssueCredentialRecordsByStatesForAllWallets(ignoreWithZeroRetries, limit, states*) + + override def getJwtIssuer( + jwtIssuerDID: PrismDID, + verificationRelationship: VerificationRelationship + ): URIO[WalletAccessContext, Issuer] = + svc.getJwtIssuer(jwtIssuerDID, verificationRelationship) } object CredentialServiceNotifier { diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala index 3a7b096bc2..e928cbbb16 100644 --- a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/MockCredentialService.scala @@ -1,12 +1,13 @@ package org.hyperledger.identus.pollux.core.service import io.circe.Json -import org.hyperledger.identus.castor.core.model.did.CanonicalPrismDID +import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID, VerificationRelationship} import org.hyperledger.identus.mercury.model.DidId import org.hyperledger.identus.mercury.protocol.issuecredential.{IssueCredential, OfferCredential, RequestCredential} import org.hyperledger.identus.pollux.core.model.{DidCommID, IssueCredentialRecord} import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError import org.hyperledger.identus.pollux.core.model.error.CredentialServiceError.* +import org.hyperledger.identus.pollux.vc.jwt.Issuer import org.hyperledger.identus.shared.models.WalletAccessContext import zio.{mock, Duration, IO, UIO, URIO, URLayer, ZIO, ZLayer} import zio.mock.{Mock, Proxy} @@ -274,6 +275,11 @@ object MockCredentialService extends Mock[CredentialService] { thid: DidCommID, ignoreWithZeroRetries: Boolean ): URIO[WalletAccessContext, Option[IssueCredentialRecord]] = ??? + + override def getJwtIssuer( + jwtIssuerDID: PrismDID, + verificationRelationship: VerificationRelationship + ): URIO[WalletAccessContext, Issuer] = ??? } } } diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/MockOID4VCIIssuerMetadataService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/MockOID4VCIIssuerMetadataService.scala new file mode 100644 index 0000000000..05e8fa9ce0 --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/MockOID4VCIIssuerMetadataService.scala @@ -0,0 +1,79 @@ +package org.hyperledger.identus.pollux.core.service + +import org.hyperledger.identus.pollux.core.model.oid4vci.{CredentialConfiguration, CredentialIssuer} +import org.hyperledger.identus.pollux.core.model.CredentialFormat +import org.hyperledger.identus.shared.models.WalletAccessContext +import zio.* +import zio.mock.{Expectation, Mock, Proxy} +import zio.test.Assertion + +import java.net.URL +import java.util.UUID + +object MockOID4VCIIssuerMetadataService extends Mock[OID4VCIIssuerMetadataService] { + + import OID4VCIIssuerMetadataServiceError.* + + object GetCredentialConfigurationById + extends Effect[ + (UUID, String), + CredentialConfigurationNotFound, + CredentialConfiguration + ] + + override val compose: URLayer[mock.Proxy, OID4VCIIssuerMetadataService] = ZLayer { + ZIO.serviceWith[Proxy] { proxy => + new OID4VCIIssuerMetadataService { + override def getCredentialIssuer(issuerId: UUID): IO[IssuerIdNotFound, CredentialIssuer] = + ZIO.die(NotImplementedError()) + + override def createCredentialIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, CredentialIssuer] = + ZIO.die(NotImplementedError()) + + override def getCredentialIssuers: URIO[WalletAccessContext, Seq[CredentialIssuer]] = + ZIO.die(NotImplementedError()) + + override def updateCredentialIssuer( + issuerId: UUID, + authorizationServer: Option[URL] = None, + authorizationServerClientId: Option[String] = None, + authorizationServerClientSecret: Option[String] = None + ): ZIO[WalletAccessContext, IssuerIdNotFound, CredentialIssuer] = ZIO.die(NotImplementedError()) + + override def deleteCredentialIssuer(issuerId: UUID): ZIO[WalletAccessContext, IssuerIdNotFound, Unit] = + ZIO.die(NotImplementedError()) + + override def createCredentialConfiguration( + issuerId: UUID, + format: CredentialFormat, + configurationId: String, + schemaId: String + ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] = + ZIO.die(NotImplementedError()) + + override def getCredentialConfigurations( + issuerId: UUID + ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] = ZIO.die(NotImplementedError()) + + override def getCredentialConfigurationById( + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, CredentialConfiguration] = + proxy(GetCredentialConfigurationById, issuerId, configurationId) + + override def deleteCredentialConfiguration( + issuerId: UUID, + configurationId: String, + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, Unit] = ZIO.die(NotImplementedError()) + } + } + } + + def getCredentialConfigurationByIdExpectations( + configuration: CredentialConfiguration + ): Expectation[OID4VCIIssuerMetadataService] = + GetCredentialConfigurationById( + assertion = Assertion.anything, + result = Expectation.value(configuration) + ) +} diff --git a/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala new file mode 100644 index 0000000000..9fe6e171fa --- /dev/null +++ b/pollux/core/src/main/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataService.scala @@ -0,0 +1,185 @@ +package org.hyperledger.identus.pollux.core.service + +import org.hyperledger.identus.pollux.core.model.error.CredentialSchemaError.{CredentialSchemaParsingError, InvalidURI} +import org.hyperledger.identus.pollux.core.model.oid4vci.{CredentialConfiguration, CredentialIssuer} +import org.hyperledger.identus.pollux.core.model.schema.CredentialSchema +import org.hyperledger.identus.pollux.core.model.CredentialFormat +import org.hyperledger.identus.pollux.core.repository.OID4VCIIssuerMetadataRepository +import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataServiceError.{ + CredentialConfigurationNotFound, + InvalidSchemaId, + IssuerIdNotFound, + UnsupportedCredentialFormat +} +import org.hyperledger.identus.shared.db.Errors.UnexpectedAffectedRow +import org.hyperledger.identus.shared.models.{Failure, StatusCode, WalletAccessContext} +import zio.* + +import java.net.{URI, URL} +import java.util.UUID + +sealed trait OID4VCIIssuerMetadataServiceError( + val statusCode: StatusCode, + val userFacingMessage: String +) extends Failure { + override val namespace: String = "OID4VCIIssuerMetadataServiceError" +} + +object OID4VCIIssuerMetadataServiceError { + final case class IssuerIdNotFound(issuerId: UUID) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.NotFound, + s"There is no credential issuer matching the provided identifier: issuerId=$issuerId" + ) + + final case class CredentialConfigurationNotFound(issuerId: UUID, configurationId: String) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.NotFound, + s"There is no credential configuration matching the provided identifier: issuerId=$issuerId, configurationId=$configurationId" + ) + + final case class InvalidSchemaId(schemaId: String, msg: String) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.BadRequest, + s"Invalid schemaId $schemaId. $msg" + ) + + final case class UnsupportedCredentialFormat(format: CredentialFormat) + extends OID4VCIIssuerMetadataServiceError( + StatusCode.BadRequest, + s"Unsupported credential format in OID4VCI protocol: $format" + ) +} + +trait OID4VCIIssuerMetadataService { + def getCredentialIssuer(issuerId: UUID): IO[IssuerIdNotFound, CredentialIssuer] + def createCredentialIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, CredentialIssuer] + def getCredentialIssuers: URIO[WalletAccessContext, Seq[CredentialIssuer]] + def updateCredentialIssuer( + issuerId: UUID, + authorizationServer: Option[URL] = None, + authorizationServerClientId: Option[String] = None, + authorizationServerClientSecret: Option[String] = None + ): ZIO[WalletAccessContext, IssuerIdNotFound, CredentialIssuer] + def deleteCredentialIssuer(issuerId: UUID): ZIO[WalletAccessContext, IssuerIdNotFound, Unit] + def createCredentialConfiguration( + issuerId: UUID, + format: CredentialFormat, + configurationId: String, + schemaId: String + ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] + def getCredentialConfigurations( + issuerId: UUID + ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] + def getCredentialConfigurationById( + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, CredentialConfiguration] + def deleteCredentialConfiguration( + issuerId: UUID, + configurationId: String, + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, Unit] +} + +class OID4VCIIssuerMetadataServiceImpl(repository: OID4VCIIssuerMetadataRepository, uriDereferencer: URIDereferencer) + extends OID4VCIIssuerMetadataService { + + override def createCredentialIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, CredentialIssuer] = + repository.createIssuer(issuer).as(issuer) + + override def getCredentialIssuers: URIO[WalletAccessContext, Seq[CredentialIssuer]] = + repository.findWalletIssuers + + override def getCredentialIssuer(issuerId: UUID): IO[IssuerIdNotFound, CredentialIssuer] = + repository + .findIssuerById(issuerId) + .someOrFail(IssuerIdNotFound(issuerId)) + + override def updateCredentialIssuer( + issuerId: UUID, + authorizationServer: Option[URL], + authorizationServerClientId: Option[String], + authorizationServerClientSecret: Option[String] + ): ZIO[WalletAccessContext, IssuerIdNotFound, CredentialIssuer] = + for { + _ <- repository + .updateIssuer( + issuerId = issuerId, + authorizationServer = authorizationServer, + authorizationServerClientId = authorizationServerClientId, + authorizationServerClientSecret = authorizationServerClientSecret + ) + .catchSomeDefect { case _: UnexpectedAffectedRow => + ZIO.fail(IssuerIdNotFound(issuerId)) + } + updatedIssuer <- getCredentialIssuer(issuerId) + } yield updatedIssuer + + override def deleteCredentialIssuer(issuerId: UUID): ZIO[WalletAccessContext, IssuerIdNotFound, Unit] = + repository + .deleteIssuer(issuerId) + .catchSomeDefect { case _: UnexpectedAffectedRow => + ZIO.fail(IssuerIdNotFound(issuerId)) + } + + override def createCredentialConfiguration( + issuerId: UUID, + format: CredentialFormat, + configurationId: String, + schemaId: String + ): ZIO[WalletAccessContext, InvalidSchemaId | UnsupportedCredentialFormat, CredentialConfiguration] = { + for { + _ <- format match { + case CredentialFormat.JWT => ZIO.unit + case f => ZIO.fail(UnsupportedCredentialFormat(f)) + } + schemaUri <- ZIO.attempt(new URI(schemaId)).mapError(t => InvalidSchemaId(schemaId, t.getMessage)) + _ <- CredentialSchema + .validSchemaValidator(schemaUri.toString(), uriDereferencer) + .catchAll { + case e: InvalidURI => ZIO.fail(InvalidSchemaId(schemaId, e.userFacingMessage)) + case e: CredentialSchemaParsingError => ZIO.fail(InvalidSchemaId(schemaId, e.cause)) + } + now <- ZIO.clockWith(_.instant) + config = CredentialConfiguration( + configurationId = configurationId, + format = CredentialFormat.JWT, + schemaId = schemaUri, + createdAt = now + ) + _ <- repository.createCredentialConfiguration(issuerId, config) + } yield config + } + + override def getCredentialConfigurations( + issuerId: UUID + ): IO[IssuerIdNotFound, Seq[CredentialConfiguration]] = + repository + .findIssuerById(issuerId) + .someOrFail(IssuerIdNotFound(issuerId)) + .flatMap(_ => repository.findCredentialConfigurationsByIssuer(issuerId)) + + override def getCredentialConfigurationById( + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, CredentialConfiguration] = + repository + .findCredentialConfigurationById(issuerId, configurationId) + .someOrFail(CredentialConfigurationNotFound(issuerId, configurationId)) + + override def deleteCredentialConfiguration( + issuerId: UUID, + configurationId: String + ): ZIO[WalletAccessContext, CredentialConfigurationNotFound, Unit] = + repository + .deleteCredentialConfiguration(issuerId, configurationId) + .catchSomeDefect { case _: UnexpectedAffectedRow => + ZIO.fail(CredentialConfigurationNotFound(issuerId, configurationId)) + } +} + +object OID4VCIIssuerMetadataServiceImpl { + def layer: URLayer[OID4VCIIssuerMetadataRepository & URIDereferencer, OID4VCIIssuerMetadataService] = { + ZLayer.fromFunction(OID4VCIIssuerMetadataServiceImpl(_, _)) + } +} diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/OID4VCIIssuerMetadataRepositorySpecSuite.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/OID4VCIIssuerMetadataRepositorySpecSuite.scala new file mode 100644 index 0000000000..26134ff767 --- /dev/null +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/repository/OID4VCIIssuerMetadataRepositorySpecSuite.scala @@ -0,0 +1,292 @@ +package org.hyperledger.identus.pollux.core.repository + +import org.hyperledger.identus.pollux.core.model.oid4vci.{CredentialConfiguration, CredentialIssuer} +import org.hyperledger.identus.pollux.core.model.CredentialFormat +import org.hyperledger.identus.shared.db.Errors.UnexpectedAffectedRow +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} +import zio.{ZIO, ZLayer} +import zio.test.* +import zio.test.Assertion.* + +import java.net.{URI, URL} +import java.time.Instant + +object OID4VCIIssuerMetadataRepositorySpecSuite { + + private val credConfig = CredentialConfiguration( + configurationId = "DrivingLicense", + format = CredentialFormat.JWT, + schemaId = URI.create("http://example.com/schema"), + createdAt = Instant.now + ).withTruncatedTimestamp() + + private def makeCredentialIssuer(authorizationServer: URL): CredentialIssuer = CredentialIssuer( + authorizationServer = authorizationServer, + clientId = "client", + clientSecret = "secret" + ) + + private def initMultiWalletIssuers = { + val walletId1 = WalletId.random + val walletId2 = WalletId.random + val wallet1 = ZLayer.succeed(WalletAccessContext(walletId1)) + val wallet2 = ZLayer.succeed(WalletAccessContext(walletId2)) + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer1 = URI.create("http://example-1.com").toURL() + authServer2 = URI.create("http://example-2.com").toURL() + issuer1 = makeCredentialIssuer(authorizationServer = authServer1) + issuer2 = makeCredentialIssuer(authorizationServer = authServer2) + _ <- repo.createIssuer(issuer1).provide(wallet1) + _ <- repo.createIssuer(issuer2).provide(wallet2) + } yield (issuer1, wallet1, issuer2, wallet2) + } + + val testSuite = suite("CRUD operations")( + test("find non-existing credential issuers return None") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + issuerId <- ZIO.randomWith(_.nextUUID) + maybeIssuer <- repo.findIssuerById(issuerId) + } yield assert(maybeIssuer)(isNone) + }, + test("create credential issuers successfully") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer1 = URI.create("http://example-1.com").toURL() + authServer2 = URI.create("http://example-2.com").toURL() + issuer1 = makeCredentialIssuer(authorizationServer = authServer1) + issuer2 = makeCredentialIssuer(authorizationServer = authServer2) + _ <- repo.createIssuer(issuer1) + _ <- repo.createIssuer(issuer2) + maybeIssuer1 <- repo.findIssuerById(issuer1.id) + maybeIssuer2 <- repo.findIssuerById(issuer2.id) + } yield assert(maybeIssuer1)(isSome(equalTo(issuer1))) && + assert(maybeIssuer2)(isSome(equalTo(issuer2))) + }, + test("create credential issuers with same id should fail") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer1 = URI.create("http://example-1.com").toURL() + issuer1 = makeCredentialIssuer(authorizationServer = authServer1) + _ <- repo.createIssuer(issuer1) + exit <- repo.createIssuer(issuer1).exit + } yield assert(exit)(dies(anything)) + }, + test("delete credential issuer successfully") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer = URI.create("http://example-1.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer) + _ <- repo.createIssuer(issuer) + _ <- repo.deleteIssuer(issuer.id) + maybeIssuer <- repo.findIssuerById(issuer.id) + } yield assert(maybeIssuer)(isNone) + }, + test("delete non-existing credential issuer should fail") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer = URI.create("http://example-1.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer) + exit <- repo.deleteIssuer(issuer.id).exit + } yield assert(exit)(dies(isSubtype[UnexpectedAffectedRow](anything))) + }, + test("update credential issuer successfully") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer1 = URI.create("http://example-1.com").toURL() + authServer2 = URI.create("http://example-2.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer1) + _ <- repo.createIssuer(issuer) + _ <- repo.updateIssuer( + issuerId = issuer.id, + authorizationServer = Some(authServer2), + authorizationServerClientId = Some("client-2"), + authorizationServerClientSecret = Some("secret-2") + ) + updatedIssuer <- repo.findIssuerById(issuer.id).some + } yield assert(updatedIssuer.id)(equalTo(issuer.id)) && + assert(updatedIssuer.authorizationServer)(equalTo(authServer2)) && + assert(updatedIssuer.authorizationServerClientId)(equalTo("client-2")) && + assert(updatedIssuer.authorizationServerClientSecret)(equalTo("secret-2")) && + assert(updatedIssuer.updatedAt)(not(equalTo(issuer.createdAt))) + }, + test("update credential issuer with empty patch successfully") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer1 = URI.create("http://example-1.com").toURL() + authServer2 = URI.create("http://example-2.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer1) + _ <- repo.createIssuer(issuer) + _ <- repo.updateIssuer(issuer.id) // empty patch + updatedIssuer <- repo.findIssuerById(issuer.id).some + } yield assert(updatedIssuer.id)(equalTo(issuer.id)) && + assert(updatedIssuer.authorizationServer)(equalTo(issuer.authorizationServer)) && + assert(updatedIssuer.createdAt)(equalTo(issuer.createdAt)) + }, + test("update non-existing credential issuer should fail") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + issuerId <- ZIO.randomWith(_.nextUUID) + authServer = URI.create("http://example-1.com").toURL() + exit <- repo.updateIssuer(issuerId, authorizationServer = Some(authServer)).exit + } yield assert(exit)(dies(isSubtype[UnexpectedAffectedRow](anything))) + }, + test("create credential configuration successfully") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer = URI.create("http://example-1.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer) + _ <- repo.createIssuer(issuer) + _ <- repo.createCredentialConfiguration(issuer.id, credConfig) + maybeCredConfig <- repo.findCredentialConfigurationById(issuer.id, credConfig.configurationId) + } yield assert(maybeCredConfig)(isSome(equalTo(credConfig))) + }, + test("create credential configuration with same id should fail") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer = URI.create("http://example-1.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer).withTruncatedTimestamp() + _ <- repo.createIssuer(issuer) + _ <- repo.createCredentialConfiguration(issuer.id, credConfig) + exit <- repo.createCredentialConfiguration(issuer.id, credConfig).exit + } yield assert(exit)(dies(anything)) + }, + test("create credential configuration with same id for different issuer successfully") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer1 = URI.create("http://example-1.com").toURL() + authServer2 = URI.create("http://example-2.com").toURL() + issuer1 = makeCredentialIssuer(authorizationServer = authServer1) + issuer2 = makeCredentialIssuer(authorizationServer = authServer2) + _ <- repo.createIssuer(issuer1) + _ <- repo.createIssuer(issuer2) + _ <- repo.createCredentialConfiguration(issuer1.id, credConfig) + _ <- repo.createCredentialConfiguration(issuer2.id, credConfig) + maybeCredConfig1 <- repo.findCredentialConfigurationById(issuer1.id, credConfig.configurationId) + maybeCredConfig2 <- repo.findCredentialConfigurationById(issuer2.id, credConfig.configurationId) + } yield assert(maybeCredConfig1)(isSome(equalTo(credConfig))) && + assert(maybeCredConfig2)(isSome(equalTo(credConfig))) + }, + test("create credential configuration for non-existing issuer should fail") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + issuerId <- ZIO.randomWith(_.nextUUID) + exit <- repo.createCredentialConfiguration(issuerId, credConfig).exit + } yield assert(exit)(dies(anything)) + }, + test("list credential configurations successfully") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer1 = URI.create("http://example-1.com").toURL() + issuer1 = makeCredentialIssuer(authorizationServer = authServer1) + issuer2 = makeCredentialIssuer(authorizationServer = authServer1) + credConfig1 = credConfig.copy(configurationId = "DrivingLicense") + credConfig2 = credConfig.copy(configurationId = "UniversityDegree") + credConfig3 = credConfig.copy(configurationId = "TrainingCertificate") + _ <- repo.createIssuer(issuer1) + _ <- repo.createIssuer(issuer2) + _ <- repo.createCredentialConfiguration(issuer1.id, credConfig1) + _ <- repo.createCredentialConfiguration(issuer1.id, credConfig2) + _ <- repo.createCredentialConfiguration(issuer2.id, credConfig3) + credConfigs1 <- repo.findCredentialConfigurationsByIssuer(issuer1.id) + credConfigs2 <- repo.findCredentialConfigurationsByIssuer(issuer2.id) + } yield assert(credConfigs1)(hasSameElements(Seq(credConfig1, credConfig2))) && + assert(credConfigs2)(hasSameElements(Seq(credConfig3))) + }, + test("find and list return empty result for non-existing configurations") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer = URI.create("http://example-1.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer) + _ <- repo.createIssuer(issuer) + maybeCredConfig <- repo.findCredentialConfigurationById(issuer.id, credConfig.configurationId) + credConfigs <- repo.findCredentialConfigurationsByIssuer(issuer.id) + } yield assert(maybeCredConfig)(isNone) && + assert(credConfigs)(isEmpty) + }, + test("delete credential configuration successfully") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer = URI.create("http://example-1.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer) + _ <- repo.createIssuer(issuer) + _ <- repo.createCredentialConfiguration(issuer.id, credConfig) + maybeCredConfig1 <- repo.findCredentialConfigurationById(issuer.id, credConfig.configurationId) + _ <- repo.deleteCredentialConfiguration(issuer.id, credConfig.configurationId) + maybeCredConfig2 <- repo.findCredentialConfigurationById(issuer.id, credConfig.configurationId) + } yield assert(maybeCredConfig1)(isSome(equalTo(credConfig))) && + assert(maybeCredConfig2)(isNone) + }, + test("delete non-existing creential configuration should fail") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer = URI.create("http://example-1.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer) + randomId <- ZIO.randomWith(_.nextUUID) + _ <- repo.createIssuer(issuer) + _ <- repo.createCredentialConfiguration(issuer.id, credConfig) + exit1 <- repo.deleteCredentialConfiguration(issuer.id, "ExampleLicense").exit + exit2 <- repo.deleteCredentialConfiguration(randomId, "ExampleLicense").exit + } yield assert(exit1)(diesWithA[UnexpectedAffectedRow]) && + assert(exit2)(diesWithA[UnexpectedAffectedRow]) + }, + test("delete issuer also delete credential configuration") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + authServer = URI.create("http://example-1.com").toURL() + issuer = makeCredentialIssuer(authorizationServer = authServer) + _ <- repo.createIssuer(issuer) + _ <- repo.createCredentialConfiguration(issuer.id, credConfig) + _ <- repo.deleteIssuer(issuer.id) + maybeCredConfig <- repo.findCredentialConfigurationById(issuer.id, credConfig.configurationId) + } yield assert(maybeCredConfig)(isNone) + } + ).provideSomeLayer(ZLayer.succeed(WalletAccessContext(WalletId.random))) + + val multitenantTestSuite = suite("multi-tenant CRUD operation")( + test("list only issuers inside wallet") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + init <- initMultiWalletIssuers + (issuer1, wallet1, issuer2, wallet2) = init + issuers1 <- repo.findWalletIssuers.provide(wallet1) + issuers2 <- repo.findWalletIssuers.provide(wallet2) + } yield assert(issuers1)(hasSameElements(Seq(issuer1))) && + assert(issuers2)(hasSameElements(Seq(issuer2))) + }, + test("update issuer across wallet is not allowed") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + init <- initMultiWalletIssuers + (issuer1, wallet1, issuer2, wallet2) = init + exit <- repo.updateIssuer(issuer1.id).provide(wallet2).exit + } yield assert(exit)(diesWithA[UnexpectedAffectedRow]) + }, + test("delete issuer across wallet is not allowed") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + init <- initMultiWalletIssuers + (issuer1, wallet1, issuer2, wallet2) = init + exit <- repo.deleteIssuer(issuer1.id).provide(wallet2).exit + } yield assert(exit)(diesWithA[UnexpectedAffectedRow]) + }, + test("create credential configuration across wallet is not allowed") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + init <- initMultiWalletIssuers + (issuer1, wallet1, issuer2, wallet2) = init + exit <- repo.createCredentialConfiguration(issuer1.id, credConfig).provide(wallet2).exit + } yield assert(exit)(dies(anything)) + }, + test("delete credential configuration across wallet is not allowed") { + for { + repo <- ZIO.service[OID4VCIIssuerMetadataRepository] + init <- initMultiWalletIssuers + (issuer1, wallet1, issuer2, wallet2) = init + exit <- repo.deleteCredentialConfiguration(issuer1.id, credConfig.configurationId).provide(wallet2).exit + } yield assert(exit)(diesWithA[UnexpectedAffectedRow]) + }, + ) + +} diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala index 722376ebb8..5b8df2ac05 100644 --- a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/CredentialServiceSpecHelper.scala @@ -28,8 +28,7 @@ trait CredentialServiceSpecHelper { protected val defaultWalletLayer = ZLayer.succeed(WalletAccessContext(WalletId.default)) protected val credentialDefinitionServiceLayer = - CredentialDefinitionRepositoryInMemory.layer ++ ResourceURIDereferencerImpl.layer >>> - CredentialDefinitionServiceImpl.layer ++ defaultWalletLayer + CredentialDefinitionRepositoryInMemory.layer >>> CredentialDefinitionServiceImpl.layer protected val credentialServiceLayer : URLayer[DIDService & ManagedDIDService & URIDereferencer, CredentialService & CredentialDefinitionService] = diff --git a/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala new file mode 100644 index 0000000000..8d2d3614bb --- /dev/null +++ b/pollux/core/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpecSuite.scala @@ -0,0 +1,124 @@ +package org.hyperledger.identus.pollux.core.service + +import org.hyperledger.identus.pollux.core.model.oid4vci.CredentialIssuer +import org.hyperledger.identus.pollux.core.model.CredentialFormat +import org.hyperledger.identus.pollux.core.service.OID4VCIIssuerMetadataServiceError.{ + CredentialConfigurationNotFound, + InvalidSchemaId, + IssuerIdNotFound +} +import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} +import zio.{ZIO, ZLayer} +import zio.test.* +import zio.test.Assertion.* + +import java.net.{URI, URL} + +object OID4VCIIssuerMetadataServiceSpecSuite { + + private def makeCredentialIssuer(authorizationServer: URL): CredentialIssuer = CredentialIssuer( + authorizationServer = authorizationServer, + clientId = "client", + clientSecret = "secret" + ) + + val testSuite = suite("OID4VCIssuerMetadataService")( + test("get credential issuer successfully") { + for { + service <- ZIO.service[OID4VCIIssuerMetadataService] + authServer1 = URI.create("http://example-1.com").toURL() + authServer2 = URI.create("http://example-2.com").toURL() + issuer1 <- service.createCredentialIssuer(makeCredentialIssuer(authServer1)) + issuer2 <- service.createCredentialIssuer(makeCredentialIssuer(authServer2)) + getIssuer1 <- service.getCredentialIssuer(issuer1.id) + getIssuer2 <- service.getCredentialIssuer(issuer2.id) + getIssuers <- service.getCredentialIssuers + } yield assert(getIssuer1)(equalTo(issuer1)) && + assert(getIssuer2)(equalTo(issuer2)) && + assert(getIssuers)(hasSameElements(Seq(issuer1, issuer2))) + }, + test("get non-existing credential issuer should fail") { + for { + service <- ZIO.service[OID4VCIIssuerMetadataService] + issuerId <- ZIO.randomWith(_.nextUUID) + issuers <- service.getCredentialIssuers + exit <- service.getCredentialIssuer(issuerId).exit + } yield assert(exit)(failsWithA[IssuerIdNotFound]) && + assert(issuers)(isEmpty) + }, + test("update credential issuer successfully") { + for { + service <- ZIO.service[OID4VCIIssuerMetadataService] + authServer = URI.create("http://example-1.com").toURL() + issuer <- service.createCredentialIssuer(makeCredentialIssuer(authServer)) + updatedAuthServer = URI.create("http://example-2.com").toURL() + _ <- service.updateCredentialIssuer(issuer.id, authorizationServer = Some(updatedAuthServer)) + updatedIssuer <- service.getCredentialIssuer(issuer.id) + } yield assert(updatedIssuer.authorizationServer)(equalTo(updatedAuthServer)) + }, + test("update non-existing credential issuer should fail") { + for { + service <- ZIO.service[OID4VCIIssuerMetadataService] + issuerId <- ZIO.randomWith(_.nextUUID) + exit <- service.updateCredentialIssuer(issuerId, Some(URI.create("http://example.com").toURL())).exit + } yield assert(exit)(failsWithA[IssuerIdNotFound]) + }, + test("create credential configuration successfully") { + for { + service <- ZIO.service[OID4VCIIssuerMetadataService] + authServer = URI.create("http://example-1.com").toURL() + issuer <- service.createCredentialIssuer(makeCredentialIssuer(authServer)) + _ <- service + .createCredentialConfiguration( + issuer.id, + CredentialFormat.JWT, + "UniversityDegree", + "resource:///vc-schema-example.json" + ) + credConfig <- service.getCredentialConfigurationById(issuer.id, "UniversityDegree") + } yield assert(credConfig.configurationId)(equalTo("UniversityDegree")) && + assert(credConfig.format)(equalTo(CredentialFormat.JWT)) && + assert(credConfig.schemaId)(equalTo(URI.create("resource:///vc-schema-example.json"))) + }, + test("create credential configuration check for schemaId validity") { + for { + service <- ZIO.service[OID4VCIIssuerMetadataService] + authServer = URI.create("http://example-1.com").toURL() + issuer <- service.createCredentialIssuer(makeCredentialIssuer(authServer)) + createCredConfig = (schemaId: String) => + service + .createCredentialConfiguration( + issuer.id, + CredentialFormat.JWT, + "UniversityDegree", + schemaId + ) + exit1 <- createCredConfig("not a uri").exit + exit2 <- createCredConfig("http://localhost/schema").exit + } yield assert(exit1)(failsWithA[InvalidSchemaId]) && + assert(exit2)(dies(anything)) + }, + test("list credential configurations for non-existing issuer should fail") { + for { + service <- ZIO.service[OID4VCIIssuerMetadataService] + issuerId <- ZIO.randomWith(_.nextUUID) + exit <- service.getCredentialConfigurations(issuerId).exit + } yield assert(exit)(failsWithA[IssuerIdNotFound]) + }, + test("get non-existing credential configuration should fail") { + for { + service <- ZIO.service[OID4VCIIssuerMetadataService] + issuerId <- ZIO.randomWith(_.nextUUID) + exit <- service.getCredentialConfigurationById(issuerId, "UniversityDegree").exit + } yield assert(exit)(failsWithA[CredentialConfigurationNotFound]) + }, + test("delete non-existing credential configuration should fail") { + for { + service <- ZIO.service[OID4VCIIssuerMetadataService] + issuerId <- ZIO.randomWith(_.nextUUID) + exit <- service.deleteCredentialConfiguration(issuerId, "UniversityDegree").exit + } yield assert(exit)(failsWithA[CredentialConfigurationNotFound]) + }, + ).provideSomeLayer(ZLayer.succeed(WalletAccessContext(WalletId.random))) + +} diff --git a/pollux/sql-doobie/src/main/resources/sql/pollux/V21__add_issuer_metadata.sql b/pollux/sql-doobie/src/main/resources/sql/pollux/V21__add_issuer_metadata.sql new file mode 100644 index 0000000000..369ba7ce1a --- /dev/null +++ b/pollux/sql-doobie/src/main/resources/sql/pollux/V21__add_issuer_metadata.sql @@ -0,0 +1,38 @@ +CREATE TABLE public.issuer_metadata ( + id UUID PRIMARY KEY, + authorization_server VARCHAR(500) NOT NULL, + authorization_server_client_id VARCHAR(100) NOT NULL, + authorization_server_client_secret VARCHAR(100) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + updated_at TIMESTAMP WITH TIME ZONE NOT NULL, + wallet_id UUID NOT NULL +); + +CREATE TABLE public.issuer_credential_configuration ( + configuration_id VARCHAR(100) NOT NULL, + issuer_id UUID NOT NULL, + format VARCHAR(9) NOT NULL, + schema_id VARCHAR(500) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE NOT NULL, + UNIQUE (configuration_id, issuer_id), + CONSTRAINT fk_issuer FOREIGN KEY (issuer_id) REFERENCES public.issuer_metadata(id) ON DELETE CASCADE +); + +ALTER TABLE public.issuer_metadata + ENABLE ROW LEVEL SECURITY; + +ALTER TABLE public.issuer_credential_configuration + ENABLE ROW LEVEL SECURITY; + +CREATE POLICY issuer_metadata_wallet_isolation + ON public.issuer_metadata + USING (wallet_id = current_setting('app.current_wallet_id')::UUID); + +CREATE POLICY issuer_credential_configuration_wallet_isolation + ON public.issuer_credential_configuration + USING ( + EXISTS (SELECT 1 + FROM public.issuer_metadata AS im + WHERE im.wallet_id = current_setting('app.current_wallet_id')::UUID + AND im.id = public.issuer_credential_configuration.issuer_id) + ); diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala index 10064bf2e5..db884407a3 100644 --- a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/Implicits.scala @@ -4,14 +4,12 @@ import doobie.util.{Get, Put} import org.hyperledger.identus.castor.core.model.did.{CanonicalPrismDID, PrismDID} import org.hyperledger.identus.pollux.core.model.* import org.hyperledger.identus.pollux.vc.jwt.StatusPurpose -import org.hyperledger.identus.shared.models.WalletId + +import java.net.{URI, URL} given didCommIDGet: Get[DidCommID] = Get[String].map(DidCommID(_)) given didCommIDPut: Put[DidCommID] = Put[String].contramap(_.value) -given walletIdGet: Get[WalletId] = Get[String].map(WalletId.fromUUIDString) -given walletIdPut: Put[WalletId] = Put[String].contramap(_.toString) - given prismDIDGet: Get[CanonicalPrismDID] = Get[String].map(s => PrismDID.fromString(s).fold(e => throw RuntimeException(e), _.asCanonical)) given prismDIDPut: Put[CanonicalPrismDID] = Put[String].contramap(_.toString) @@ -26,3 +24,12 @@ given statusPurposePut: Put[StatusPurpose] = Put[String].contramap { case StatusPurpose.Revocation => StatusPurpose.Revocation.str case StatusPurpose.Suspension => StatusPurpose.Suspension.str } + +given urlGet: Get[URL] = Get[String].map(s => URI.create(s).toURL()) +given urlPut: Put[URL] = Put[String].contramap(_.toString()) + +given uriGet: Get[URI] = Get[String].map(s => URI.create(s)) +given uriPut: Put[URI] = Put[String].contramap(_.toString()) + +given credFormatGet: Get[CredentialFormat] = Get[String].map(CredentialFormat.valueOf) +given credFormatPut: Put[CredentialFormat] = Put[String].contramap(_.toString()) 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 57c5fea44c..177d44dd59 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 @@ -12,6 +12,7 @@ import org.hyperledger.identus.pollux.vc.jwt.revocation.{BitString, BitStringErr import org.hyperledger.identus.pollux.vc.jwt.revocation.BitStringError.* import org.hyperledger.identus.shared.db.ContextAwareTask import org.hyperledger.identus.shared.db.Implicits.* +import org.hyperledger.identus.shared.db.Implicits.{*, given} import org.hyperledger.identus.shared.models.{WalletAccessContext, WalletId} import zio.* import zio.interop.catz.* diff --git a/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcOID4VCIIssuerMetadataRepository.scala b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcOID4VCIIssuerMetadataRepository.scala new file mode 100644 index 0000000000..b43fe8f70e --- /dev/null +++ b/pollux/sql-doobie/src/main/scala/org/hyperledger/identus/pollux/sql/repository/JdbcOID4VCIIssuerMetadataRepository.scala @@ -0,0 +1,210 @@ +package org.hyperledger.identus.pollux.sql.repository + +import doobie.* +import doobie.implicits.* +import doobie.postgres.implicits.* +import doobie.util.transactor.Transactor +import org.hyperledger.identus.pollux.core.model.oid4vci.{CredentialConfiguration, CredentialIssuer} +import org.hyperledger.identus.pollux.core.repository.OID4VCIIssuerMetadataRepository +import org.hyperledger.identus.shared.db.ContextAwareTask +import org.hyperledger.identus.shared.db.Implicits.* +import org.hyperledger.identus.shared.models.WalletAccessContext +import zio.* +import zio.interop.catz.* + +import java.net.URL +import java.time.Instant +import java.util.UUID + +class JdbcOID4VCIIssuerMetadataRepository(xa: Transactor[ContextAwareTask], xb: Transactor[Task]) + extends OID4VCIIssuerMetadataRepository { + + override def findIssuerById(issuerId: UUID): UIO[Option[CredentialIssuer]] = { + val cxnIO = sql""" + |SELECT + | id, + | authorization_server, + | authorization_server_client_id, + | authorization_server_client_secret, + | created_at, + | updated_at + |FROM public.issuer_metadata + |WHERE id = $issuerId + """.stripMargin + .query[CredentialIssuer] + .option + + cxnIO + .transact(xb) + .orDie + } + + override def findWalletIssuers: URIO[WalletAccessContext, Seq[CredentialIssuer]] = { + val cxnIO = sql""" + |SELECT + | id, + | authorization_server, + | authorization_server_client_id, + | authorization_server_client_secret, + | created_at, + | updated_at + |FROM public.issuer_metadata + """.stripMargin + .query[CredentialIssuer] + .to[Seq] + + cxnIO + .transactWallet(xa) + .orDie + } + + override def createIssuer(issuer: CredentialIssuer): URIO[WalletAccessContext, Unit] = { + val cxnIO = sql""" + |INSERT INTO public.issuer_metadata ( + | id, + | authorization_server, + | authorization_server_client_id, + | authorization_server_client_secret, + | created_at, + | updated_at, + | wallet_id + |) VALUES ( + | ${issuer.id}, + | ${issuer.authorizationServer}, + | ${issuer.authorizationServerClientId}, + | ${issuer.authorizationServerClientSecret}, + | ${issuer.createdAt}, + | ${issuer.updatedAt}, + | current_setting('app.current_wallet_id')::UUID + |) + """.stripMargin.update + + cxnIO.run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } + + override def updateIssuer( + issuerId: UUID, + authorizationServer: Option[URL], + authorizationServerClientId: Option[String], + authorizationServerClientSecret: Option[String] + ): URIO[WalletAccessContext, Unit] = { + val setFr = (now: Instant) => + Fragments.set( + fr"updated_at = $now", + (Seq( + authorizationServer.map(url => fr"authorization_server = $url"), + authorizationServerClientId.map(i => fr"authorization_server_client_id = $i"), + authorizationServerClientSecret.map(i => fr"authorization_server_client_secret = $i") + ).flatten)* + ) + val cxnIO = (setFr: Fragment) => sql""" + |UPDATE public.issuer_metadata + |$setFr + |WHERE id = $issuerId + """.stripMargin.update + + for { + now <- ZIO.clockWith(_.instant) + _ <- cxnIO(setFr(now)).run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } yield () + } + + override def deleteIssuer(issuerId: UUID): URIO[WalletAccessContext, Unit] = { + val cxnIO = sql""" + | DELETE FROM public.issuer_metadata + | WHERE id = $issuerId + """.stripMargin.update + + cxnIO.run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } + + override def createCredentialConfiguration( + issuerId: UUID, + config: CredentialConfiguration + ): URIO[WalletAccessContext, Unit] = { + val cxnIO = sql""" + |INSERT INTO public.issuer_credential_configuration ( + | configuration_id, + | issuer_id, + | format, + | schema_id, + | created_at + |) VALUES ( + | ${config.configurationId}, + | ${issuerId}, + | ${config.format}, + | ${config.schemaId}, + | ${config.createdAt} + |) + """.stripMargin.update + + cxnIO.run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } + + override def findCredentialConfigurationsByIssuer(issuerId: UUID): UIO[Seq[CredentialConfiguration]] = { + val cxnIO = sql""" + |SELECT + | configuration_id, + | format, + | schema_id, + | created_at + |FROM public.issuer_credential_configuration + |WHERE issuer_id = $issuerId + """.stripMargin + .query[CredentialConfiguration] + .to[Seq] + + cxnIO + .transact(xb) + .orDie + } + + override def findCredentialConfigurationById( + issuerId: UUID, + configurationId: String + ): URIO[WalletAccessContext, Option[CredentialConfiguration]] = { + val cxnIO = sql""" + |SELECT + | configuration_id, + | format, + | schema_id, + | created_at + |FROM public.issuer_credential_configuration + |WHERE issuer_id = $issuerId AND configuration_id = $configurationId + """.stripMargin + .query[CredentialConfiguration] + .option + + cxnIO + .transactWallet(xa) + .orDie + } + + override def deleteCredentialConfiguration( + issuerId: UUID, + configurationId: String + ): URIO[WalletAccessContext, Unit] = { + val cxnIO = sql""" + | DELETE FROM public.issuer_credential_configuration + | WHERE issuer_id = $issuerId AND configuration_id = $configurationId + """.stripMargin.update + + cxnIO.run + .transactWallet(xa) + .ensureOneAffectedRowOrDie + } + +} + +object JdbcOID4VCIIssuerMetadataRepository { + val layer: URLayer[Transactor[ContextAwareTask] & Transactor[Task], OID4VCIIssuerMetadataRepository] = + ZLayer.fromFunction(new JdbcOID4VCIIssuerMetadataRepository(_, _)) +} diff --git a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala new file mode 100644 index 0000000000..bdae719bd9 --- /dev/null +++ b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/core/service/OID4VCIIssuerMetadataServiceSpec.scala @@ -0,0 +1,33 @@ +package org.hyperledger.identus.pollux.core.service + +import org.hyperledger.identus.pollux.sql.repository.JdbcOID4VCIIssuerMetadataRepository +import org.hyperledger.identus.sharedtest.containers.PostgresTestContainerSupport +import org.hyperledger.identus.test.container.MigrationAspects +import zio.* +import zio.test.* + +object OID4VCIIssuerMetadataServiceSpec extends ZIOSpecDefault, PostgresTestContainerSupport { + + private val migration = MigrationAspects.migrateEach( + schema = "public", + paths = "classpath:sql/pollux" + ) + + private val testEnvironmentLayer = ZLayer.make[OID4VCIIssuerMetadataService]( + OID4VCIIssuerMetadataServiceImpl.layer, + JdbcOID4VCIIssuerMetadataRepository.layer, + ResourceURIDereferencerImpl.layer, + contextAwareTransactorLayer, + systemTransactorLayer + ) + + override def spec = + (suite("OID4VCIIssuerMetadataService - Jdbc repository")( + OID4VCIIssuerMetadataServiceSpecSuite.testSuite + ) @@ migration).provide( + Runtime.removeDefaultLoggers, + testEnvironmentLayer, + pgContainerLayer, + ) + +} diff --git a/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/repository/JdbcOID4VCIIssuerMetadataRepositorySpec.scala b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/repository/JdbcOID4VCIIssuerMetadataRepositorySpec.scala new file mode 100644 index 0000000000..36390023d1 --- /dev/null +++ b/pollux/sql-doobie/src/test/scala/org/hyperledger/identus/pollux/sql/repository/JdbcOID4VCIIssuerMetadataRepositorySpec.scala @@ -0,0 +1,34 @@ +package org.hyperledger.identus.pollux.sql.repository + +import org.hyperledger.identus.pollux.core.repository.{ + OID4VCIIssuerMetadataRepository, + OID4VCIIssuerMetadataRepositorySpecSuite +} +import org.hyperledger.identus.sharedtest.containers.PostgresTestContainerSupport +import org.hyperledger.identus.test.container.MigrationAspects +import zio.* +import zio.test.* + +object JdbcOID4VCIIssuerMetadataRepositorySpec extends ZIOSpecDefault, PostgresTestContainerSupport { + + private val migration = MigrationAspects.migrateEach( + schema = "public", + paths = "classpath:sql/pollux" + ) + + private val testEnvironmentLayer = ZLayer.make[OID4VCIIssuerMetadataRepository]( + JdbcOID4VCIIssuerMetadataRepository.layer, + contextAwareTransactorLayer, + systemTransactorLayer + ) + + override def spec = + (suite("JdbcOID4VCIIssuerMetadataRepository")( + OID4VCIIssuerMetadataRepositorySpecSuite.testSuite, + OID4VCIIssuerMetadataRepositorySpecSuite.multitenantTestSuite, + ) @@ migration).provide( + Runtime.removeDefaultLoggers, + testEnvironmentLayer, + pgContainerLayer, + ) +} 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 3e7f5bc815..6bc4494aaf 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,12 +1,14 @@ package org.hyperledger.identus.pollux.vc.jwt import com.nimbusds.jose.{JWSAlgorithm, JWSHeader} +import com.nimbusds.jose.{JOSEObjectType, JWSAlgorithm, JWSHeader} import com.nimbusds.jose.crypto.{ECDSASigner, Ed25519Signer} import com.nimbusds.jose.crypto.bc.BouncyCastleProviderSingleton import com.nimbusds.jose.jwk.{Curve, ECKey} import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT} import io.circe.* import org.hyperledger.identus.shared.crypto.Ed25519KeyPair +import org.hyperledger.identus.shared.crypto.{Ed25519KeyPair, Secp256k1PrivateKey} import zio.* import java.security.* @@ -21,6 +23,19 @@ object JWT { } } +object JwtSignerImplicits { + import com.nimbusds.jose.JWSSigner + + implicit class JwtSignerProviderSecp256k1(secp256k1PrivateKey: Secp256k1PrivateKey) { + def asJwtSigner: JWSSigner = { + val ecdsaSigner = ECDSASigner(secp256k1PrivateKey.toJavaPrivateKey, Curve.SECP256K1) + val bouncyCastleProvider = BouncyCastleProviderSingleton.getInstance + ecdsaSigner.getJCAContext.setProvider(bouncyCastleProvider) + ecdsaSigner + } + } +} + trait Signer { def encode(claim: Json): JWT @@ -45,7 +60,28 @@ class ES256KSigner(privateKey: PrivateKey) extends Signer { override def encode(claim: Json): JWT = { val claimSet = JWTClaimsSet.parse(claim.noSpaces) val signedJwt = SignedJWT( - new JWSHeader.Builder(JWSAlgorithm.ES256K).build(), + new JWSHeader.Builder(JWSAlgorithm.ES256K).`type`(JOSEObjectType.JWT).build(), + claimSet + ) + signedJwt.sign(signer) + JWT(signedJwt.serialize()) + } +} + +class EdSigner(ed25519KeyPair: Ed25519KeyPair) extends Signer { + lazy val signer: Ed25519Signer = { + val ed25519Signer = Ed25519Signer(ed25519KeyPair.toOctetKeyPair) + 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) 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 4e5e0a53b7..99af6549ee 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 @@ -177,3 +177,7 @@ class PrismDidResolver(didService: DIDService) extends DidResolver { } } + +object PrismDidResolver { + val layer: URLayer[DIDService, DidResolver] = ZLayer.fromFunction(PrismDidResolver(_)) +} diff --git a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerification.scala b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerification.scala index 2ffb16eab6..9e48db7677 100644 --- a/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerification.scala +++ b/pollux/vc-jwt/src/main/scala/org/hyperledger/identus/pollux/vc/jwt/JWTVerification.scala @@ -9,6 +9,7 @@ import com.nimbusds.jwt.SignedJWT import io.circe import io.circe.generic.auto.* import org.hyperledger.identus.castor.core.model.did.VerificationRelationship +import org.hyperledger.identus.castor.core.model.did.{PrismDID, VerificationRelationship} import org.hyperledger.identus.shared.crypto.Ed25519PublicKey import pdi.jwt.* import zio.* @@ -85,6 +86,17 @@ object JWTVerification { loadDidDocument } + def validateIssuerFromKeyId( + extractedDID: Validation[String, String] + )(didResolver: DidResolver): IO[String, Validation[String, DIDDocument]] = { + val loadDidDocument = + ValidationUtils + .foreach(extractedDID.map(validIssuerDid => resolve(validIssuerDid)(didResolver)))(identity) + .map(b => b.flatten) + + loadDidDocument + } + def validateEncodedJwt[T](jwt: JWT, proofPurpose: Option[VerificationRelationship] = None)( didResolver: DidResolver )(decoder: String => Validation[String, T])(issuerDidExtractor: T => String): IO[String, Validation[String, Unit]] = { @@ -121,6 +133,61 @@ object JWTVerification { }) } + def keyIdDIDExtractor(jwt: JWT): Validation[String, String] = { + for { + header <- Validation + .fromTry( + JwtCirce + .decodeAll(jwt.value, JwtOptions(false, false, false)) + .map(_._1) + ) + .mapError(_.getMessage) + keyId <- Validation.fromOptionWith("Key ID not found in JWT header")(header.keyId) + isValidPrismDID <- Validation.fromEither(PrismDID.fromString(keyId)) // TODO: we don't support other DIDs yet + } yield keyId + } + + def validateEncodedJwtWithKeyId( + jwt: JWT, + proofPurpose: Option[VerificationRelationship] = None, + didResolver: DidResolver, + didExtractor: JWT => Validation[String, String] = keyIdDIDExtractor + ): IO[String, Validation[String, Unit]] = { + val decodedJWT = Validation + .fromTry(JwtCirce.decodeRawAll(jwt.value, JwtOptions(false, false, false))) + .mapError(_.getMessage) + + val extractAlgorithm: Validation[String, JwtAlgorithm] = + for { + decodedJwtTask <- decodedJWT + (header, _, _) = decodedJwtTask + algorithm <- Validation + .fromOptionWith("An algorithm must be specified in the header")(JwtCirce.parseHeader(header).algorithm) + } yield algorithm + + val claim: Validation[String, String] = + for { + decodedJwtTask <- decodedJWT + (_, claim, _) = decodedJwtTask + } yield claim + + val extractedDID = didExtractor(jwt) + + val loadDidDocument = validateIssuerFromKeyId(extractedDID)(didResolver) + + loadDidDocument + .map(validatedDidDocument => { + for { + results <- Validation.validateWith(validatedDidDocument, extractAlgorithm)((didDocument, algorithm) => + (didDocument, algorithm) + ) + (didDocument, algorithm) = results + verificationMethods <- extractVerificationMethods(didDocument, algorithm, proofPurpose) + validatedJwt <- validateEncodedJwt(jwt, verificationMethods) + } yield validatedJwt + }) + } + def toECDSAVerifier(publicKey: PublicKey): JWSVerifier = { val verifier: JWSVerifier = publicKey match { case key: ECPublicKey => ECDSAVerifier(key) 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 232661d1b0..e8682e0ac3 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,6 +19,9 @@ import java.security.PublicKey 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 + opaque type DID = String object DID { def apply(value: String): DID = value diff --git a/shared/core/src/main/scala/org/hyperledger/identus/shared/db/ContextAwareTask.scala b/shared/core/src/main/scala/org/hyperledger/identus/shared/db/ContextAwareTask.scala index c42fc9a684..3b07e0be97 100644 --- a/shared/core/src/main/scala/org/hyperledger/identus/shared/db/ContextAwareTask.scala +++ b/shared/core/src/main/scala/org/hyperledger/identus/shared/db/ContextAwareTask.scala @@ -13,6 +13,10 @@ import java.util.UUID trait ContextAware type ContextAwareTask[T] = Task[T] & ContextAware +object Errors { + final case class UnexpectedAffectedRow(count: Int) extends RuntimeException(s"Unexpected affected row count: $count") +} + object Implicits { given walletIdGet: Get[WalletId] = Get[UUID].map(WalletId.fromUUID) @@ -43,10 +47,10 @@ object Implicits { } - extension [Int](ma: RIO[WalletAccessContext, Int]) { + extension (ma: RIO[WalletAccessContext, Int]) { def ensureOneAffectedRowOrDie: URIO[WalletAccessContext, Unit] = ma.flatMap { case 1 => ZIO.unit - case count => ZIO.fail(RuntimeException(s"Unexpected affected row count: $count")) + case count => ZIO.fail(Errors.UnexpectedAffectedRow(count)) }.orDie }