diff --git a/.env-ui.sample b/.env-ui.sample
index 94b376b66..e6d839ca1 100644
--- a/.env-ui.sample
+++ b/.env-ui.sample
@@ -4,5 +4,5 @@ ISSUER_UI_BUILD_TAG=
ISSUER_UI_WARNING_MESSAGE=
ISSUER_UI_IPFS_GATEWAY_URL=https://ipfs-proxy-cache.privado.id
ISSUER_UI_SCHEMA_EXPLORER_AND_BUILDER_URL=https://tools.privado.id
-ISSUER_UI_SCHEMA_EXPLORER_AND_BUILDER_URL=https://display-method-dev.privado.id
+ISSUER_UI_DISPLAY_METHOD_BUILDER_URL=https://display-method-dev.privado.id
ISSUER_UI_INSECURE=false
\ No newline at end of file
diff --git a/.github/workflows/delete_testing_env.yml b/.github/workflows/delete_testing_env.yml
index 21718f71b..68f220d18 100644
--- a/.github/workflows/delete_testing_env.yml
+++ b/.github/workflows/delete_testing_env.yml
@@ -1,9 +1,14 @@
name: Delete Helm Release
-on: [delete]
+on:
+ workflow_dispatch:
+ inputs:
+ branch:
+ description: 'Branch to target'
+ required: true
env:
- BRANCH_NAME: ${{ github.event.ref }}
+ BRANCH_NAME: ${{ github.event.inputs.branch }}
jobs:
delete:
@@ -24,14 +29,14 @@ jobs:
echo "Extracted URL: $url"
echo "::set-output name=url::$url"
- - name: Cambiar contexto de kubectl
+ - name: change k3s context
run: |
kubectl config use-context k3s
- - name: Verificar conexión al clúster
+ - name: check if the cluster exists
run: kubectl cluster-info
- - name: Check if helm chart exists
+ - name: check if helm chart exists
id: helm_check
run: |
result=$(helm list --namespace "${{ steps.extract-url.outputs.url }}" -q | grep "^${{ steps.extract-url.outputs.url }}$" || echo 'not_found')
@@ -48,7 +53,7 @@ jobs:
overrule_existing_kubeconfig: "true"
if: steps.helm_check.outputs.result != 'not_found'
- - name: "Delete namespace"
+ - name: "delete namespace"
run: |
kubectl delete namespace ${{ env.BRANCH_NAME }}
if: steps.helm_check.outputs.result != 'not_found'
diff --git a/.github/workflows/deploy_testing_env.yml b/.github/workflows/deploy_testing_env.yml
index 499288f23..1a7c75060 100644
--- a/.github/workflows/deploy_testing_env.yml
+++ b/.github/workflows/deploy_testing_env.yml
@@ -114,7 +114,7 @@ jobs:
exec: helm install "${{ steps.extract-url.outputs.url }}" --create-namespace ./k8s/helm --wait --atomic --timeout 5m --namespace="${{ steps.extract-url.outputs.url }}" --values=./k8s/helm/values.yaml --set apidomain="${{ env.API_DOMAIN }}" --set uidomain="${{ env.UI_DOMAIN }}" --set privateKey=${{ secrets.ISSUER_NODE_TESTING_PRIVATE_KEY }} --set ingressEnabled="true" --set vaultpwd="foo" --set issuerUiInsecure=true --set issuerResolverFile="${{ secrets.ISSUER_NODE_TESTING_ISSUER_RESOLVER_FILE }}" --set issuernode_repository_image="${{ env.ISSUER_NODE_API_IMAGE }}" --set issuernode_repository_tag="${{ steps.version.outputs.VERSION }}" --set issuernode_ui_repository_image="${{ env.ISSUER_NODE_UI_IMAGE }}" --set issuernode_ui_repository_tag="${{ steps.version.outputs.VERSION }}"
kubeconfig: "${{ secrets.KUBECONFIG }}"
overrule_existing_kubeconfig: "true"
- if: steps.helm_check.outputs.result == 'not_found'
+ if: steps.helm_check.outputs.result == 'not_found' && steps.extract-url.outputs.url != ''
- name: "Update helm chart"
uses: WyriHaximus/github-action-helm3@v3.0
@@ -122,4 +122,4 @@ jobs:
exec: helm upgrade "${{ steps.extract-url.outputs.url }}" ./k8s/helm/ --wait --atomic --timeout 5m --namespace="${{ steps.extract-url.outputs.url }}" --values=./k8s/helm/values.yaml --set apidomain="${{ env.API_DOMAIN }}" --set uidomain="${{ env.UI_DOMAIN }}" --set privateKey=${{ secrets.ISSUER_NODE_TESTING_PRIVATE_KEY }} --set ingressEnabled="true" --set vaultpwd="foo" --set issuerUiInsecure=true --set issuerResolverFile="${{ secrets.ISSUER_NODE_TESTING_ISSUER_RESOLVER_FILE }}" --set issuernode_repository_image="${{ env.ISSUER_NODE_API_IMAGE }}" --set issuernode_repository_tag="${{ steps.version.outputs.VERSION }}" --set issuernode_ui_repository_image="${{ env.ISSUER_NODE_UI_IMAGE }}" --set issuernode_ui_repository_tag="${{ steps.version.outputs.VERSION }}"
kubeconfig: "${{ secrets.KUBECONFIG }}"
overrule_existing_kubeconfig: "true"
- if: steps.helm_check.outputs.result != 'not_found'
\ No newline at end of file
+ if: steps.helm_check.outputs.result != 'not_found' && steps.extract-url.outputs.url != ''
\ No newline at end of file
diff --git a/api/api.yaml b/api/api.yaml
index 7b293854b..b65a6fbcd 100644
--- a/api/api.yaml
+++ b/api/api.yaml
@@ -26,6 +26,8 @@ tags:
description: Collection of endpoints related to Mobile
- name: config
description: Collection of endpoints related to Config
+ - name: Key Management
+ description: Collection of endpoints related to Key Management
paths:
@@ -425,6 +427,51 @@ paths:
'500':
$ref: '#/components/responses/500'
+ /v2/identities/{identifier}/create-auth-credential:
+ post:
+ summary: Create Auth Credential
+ operationId: CreateAuthCredential
+ description: |
+ Endpoint to create a new Auth Credential
+ * keyID - only babyjubjub keys supported
+
+ security:
+ - basicAuth: [ ]
+ parameters:
+ - $ref: '#/components/parameters/pathIdentifier2'
+ tags:
+ - Identity
+ - Key Management
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateAuthCredentialRequest'
+ responses:
+ '201':
+ description: Key added successfully
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - id
+ - credentialStatusType
+ properties:
+ id:
+ type: string
+ description: The ID of the created Auth Credential
+ x-go-type: uuid.UUID
+ x-go-type-import:
+ name: uuid
+ path: github.com/google/uuid
+ example: 8edd8112-c415-11ed-b036-debe37e1cbd6
+ '400':
+ $ref: '#/components/responses/400'
+ '500':
+ $ref: '#/components/responses/500'
+
#connections:
/v2/identities/{identifier}/connections/{id}:
get:
@@ -1469,6 +1516,179 @@ paths:
$ref: '#/components/responses/404'
'500':
$ref: '#/components/responses/500'
+ /v2/identities/{identifier}/keys:
+ post:
+ summary: Create a Key
+ operationId: CreateKey
+ description: Endpoint to create a new key.
+ tags:
+ - Key Management
+ security:
+ - basicAuth: [ ]
+ parameters:
+ - $ref: '#/components/parameters/pathIdentifier2'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateKeyRequest'
+ responses:
+ '201':
+ description: Crated Key
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/CreateKeyResponse'
+ '400':
+ $ref: '#/components/responses/400'
+ '401':
+ $ref: '#/components/responses/401'
+ '500':
+ $ref: '#/components/responses/500'
+
+ get:
+ summary: Get Keys
+ operationId: GetKeys
+ description: |
+ Returns a list of Keys for the provided identity.
+ security:
+ - basicAuth: [ ]
+ tags:
+ - Key Management
+ parameters:
+ - $ref: '#/components/parameters/pathIdentifier2'
+ - in: query
+ name: max_results
+ schema:
+ type: integer
+ format: uint
+ example: 50
+ default: 50
+ description: Number of items to fetch on each page. Minimum is 10. Default is 50. No maximum by the moment.
+ - in: query
+ name: page
+ schema:
+ type: integer
+ format: uint
+ minimum: 1
+ example: 1
+ description: Page to fetch. First is one. If omitted, page 1 will be returned.
+ - in: query
+ name: type
+ schema:
+ type: string
+ x-omitempty: false
+ example: "babyjubJub"
+ enum: [ babyjubJub, secp256k1 ]
+ description: If not provided, all keys will be returned.
+ responses:
+ '200':
+ description: Keys collection
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/KeysPaginated'
+ '400':
+ $ref: '#/components/responses/400'
+ '404':
+ $ref: '#/components/responses/404'
+ '500':
+ $ref: '#/components/responses/500'
+
+ /v2/identities/{identifier}/keys/{id}:
+ get:
+ summary: Get a Key
+ operationId: GetKey
+ description: Get a specific key for the provided identity.
+ tags:
+ - Key Management
+ security:
+ - basicAuth: [ ]
+ parameters:
+ - $ref: '#/components/parameters/pathIdentifier2'
+ - $ref: '#/components/parameters/pathKeyID'
+ responses:
+ '200':
+ description: Key found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/Key'
+ '400':
+ $ref: '#/components/responses/400'
+ '401':
+ $ref: '#/components/responses/401'
+ '404':
+ $ref: '#/components/responses/404'
+ '500':
+ $ref: '#/components/responses/500'
+ patch:
+ summary: Update a Key
+ operationId: UpdateKey
+ description: Update a specific key.
+ tags:
+ - Key Management
+ security:
+ - basicAuth: [ ]
+ parameters:
+ - $ref: '#/components/parameters/pathIdentifier2'
+ - $ref: '#/components/parameters/pathKeyID'
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ required:
+ - name
+ properties:
+ name:
+ type: string
+ example: "New Key Name"
+ responses:
+ '200':
+ description: Key found
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericMessage'
+ '400':
+ $ref: '#/components/responses/400'
+ '401':
+ $ref: '#/components/responses/401'
+ '404':
+ $ref: '#/components/responses/404'
+ '500':
+ $ref: '#/components/responses/500'
+ delete:
+ summary: Delete Key
+ operationId: DeleteKey
+ description: Remove a specific key for the provided identity.
+ tags:
+ - Identity
+ security:
+ - basicAuth: [ ]
+ parameters:
+ - $ref: '#/components/parameters/pathIdentifier2'
+ - $ref: '#/components/parameters/pathKeyID'
+ responses:
+ '200':
+ description: Key deleted
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/GenericMessage'
+ '400':
+ $ref: '#/components/responses/400'
+ '404':
+ $ref: '#/components/responses/404'
+ '401':
+ $ref: '#/components/responses/401'
+ '500':
+ $ref: '#/components/responses/500'
+
+
/v2/identities/{identifier}/payment-request:
get:
@@ -1853,6 +2073,7 @@ components:
- status
- keyType
- credentialStatusType
+ - authCredentialsIDs
properties:
identifier:
type: string
@@ -1873,6 +2094,10 @@ components:
type: string
example: "Iden3ReverseSparseMerkleTreeProof"
enum: [ Iden3commRevocationStatusV1.0, Iden3ReverseSparseMerkleTreeProof, Iden3OnchainSparseMerkleTreeProof2023 ]
+ authCredentialsIDs:
+ type: array
+ items:
+ type: string
IdentityState:
type: object
@@ -2856,7 +3081,7 @@ components:
issuerDoc:
type: object
format: byte
-
+
SupportedNetworks:
type: object
x-omitempty: false
@@ -2895,22 +3120,113 @@ components:
path: github.com/iden3/iden3comm/v2/protocol
CreateDisplayMethodRequest:
- type: object
- required:
- - name
- - url
- properties:
- name:
- type: string
- example: "My Display Method"
- url:
- type: string
- example: "https://my-display-method.com"
- type:
- type: string
- example: "Iden3BasicDisplayMethodV1"
- description: "Display method type (Iden3BasicDisplayMethodV1 is default value)"
+ type: object
+ required:
+ - name
+ - url
+ properties:
+ name:
+ type: string
+ example: "My Display Method"
+ url:
+ type: string
+ example: "https://my-display-method.com"
+ type:
+ type: string
+ example: "Iden3BasicDisplayMethodV1"
+ description: "Display method type (Iden3BasicDisplayMethodV1 is default value)"
+
+ CreateKeyRequest:
+ type: object
+ required:
+ - keyType
+ - name
+ properties:
+ keyType:
+ type: string
+ x-omitempty: false
+ example: "babyjubJub"
+ enum: [ babyjubJub, secp256k1 ]
+ name:
+ type: string
+ example: "my key"
+
+ CreateKeyResponse:
+ type: object
+ required:
+ - id
+ properties:
+ id:
+ type: string
+ x-omitempty: false
+ description: base64 encoded keyID
+ example: a2V5cy9kaWQ6aWRlbjM6cG9seWdvbjphbW95OnhKQktvbkJ1dWdKbW1aMkdvS2gzOTM
+
+ Key:
+ type: object
+ required:
+ - id
+ - keyType
+ - publicKey
+ - isAuthCredential
+ - name
+ properties:
+ id:
+ type: string
+ x-omitempty: false
+ example: ZGlkOnBvbHlnb25pZDpwb2x5Z29uOmFtb3k6MnFRNjhKa1JjZjN5cXBYanRqVVQ3WjdVeW1TV0hzYll
+ description: base64 encoded keyID
+ keyType:
+ type: string
+ x-omitempty: false
+ example: "babyjubJub"
+ enum: [ babyjubJub, secp256k1 ]
+ publicKey:
+ type: string
+ x-omitempty: false
+ example: "0x04e3e7e"
+ isAuthCredential:
+ type: boolean
+ x-omitempty: false
+ example: true
+ name:
+ type: string
+ x-omitempty: false
+ example: "my key"
+ KeysPaginated:
+ type: object
+ required: [ items, meta ]
+ properties:
+ items:
+ type: array
+ items:
+ $ref: '#/components/schemas/Key'
+ meta:
+ $ref: '#/components/schemas/PaginatedMetadata'
+
+ CreateAuthCredentialRequest:
+ type: object
+ required: [keyID, credentialStatusType]
+ properties:
+ keyID:
+ type: string
+ x-omitempty: false
+ example: ZGlkOnBvbHlnb25pZDpwb2x5Z29uOmFtb3k6MnFRNjhKa1JjZjN5cXBYanRqVVQ3WjdVeW1TV0hzYll
+ expiration:
+ type: integer
+ format: int64
+ version:
+ type: integer
+ format: uint32
+ revNonce:
+ type: integer
+ format: uint64
+ credentialStatusType:
+ type: string
+ x-omitempty: true
+ example: "Iden3ReverseSparseMerkleTreeProof"
+ enum: [ Iden3commRevocationStatusV1.0, Iden3ReverseSparseMerkleTreeProof, Iden3OnchainSparseMerkleTreeProof2023 ]
parameters:
credentialStatusType:
@@ -2969,6 +3285,18 @@ components:
schema:
type: string
+ pathIdentifier2:
+ name: identifier
+ in: path
+ required: true
+ description: Issuer identifier
+ schema:
+ type: string
+ x-go-type: Identity
+ x-go-type-import:
+ name: customIdentity
+ path: github.com/polygonid/sh-id-platform/internal/api
+
pathClaim:
name: id
in: path
@@ -2986,6 +3314,15 @@ components:
type: integer
format: int64
+ pathKeyID:
+ name: id
+ in: path
+ required: true
+ description: Key ID in base64
+ schema:
+ type: string
+
+
responses:
'400':
description: 'Bad Request'
@@ -3059,4 +3396,4 @@ components:
code:
type: integer
error:
- type: string
+ type: string
\ No newline at end of file
diff --git a/api/spec.html b/api/spec.html
index f653c5747..ae3fa7a07 100644
--- a/api/spec.html
+++ b/api/spec.html
@@ -9,7 +9,7 @@
= len(keyIDs) {
+ return []*ports.KMSKey{}, 0, nil
+ }
+
+ if end > len(keyIDs) {
+ end = len(keyIDs)
+ }
+
+ keys := make([]*ports.KMSKey, len(keyIDs))
+ for i, keyID := range keyIDs {
+ key, err := ks.Get(ctx, did, keyID.ID)
+ if err != nil {
+ log.Error(ctx, "failed to get key", "err", err)
+ return nil, 0, err
+ }
+ keys[i] = key
+ }
+
+ sort.Slice(keys, func(i, j int) bool {
+ return keys[i].Name < keys[j].Name
+ })
+
+ keys = keys[start:end]
+ return keys, total, nil
+}
+
+// Delete deletes the key with the given keyID
+func (ks *Key) Delete(ctx context.Context, did *w3c.DID, keyID string) error {
+ keyType, err := getKeyType(keyID)
+ if err != nil {
+ log.Error(ctx, "failed to get key type", "err", err)
+ return err
+ }
+
+ kmsKeyID := kms.KeyID{
+ ID: keyID,
+ Type: keyType,
+ }
+
+ exists, err := ks.kms.Exists(ctx, kmsKeyID)
+ if err != nil {
+ log.Error(ctx, "failed to check if key exists", "err", err)
+ return err
+ }
+
+ if !exists {
+ return ErrKeyNotFound
+ }
+
+ publicKey, err := ks.getPublicKey(ctx, keyID)
+ if err != nil {
+ log.Error(ctx, "failed to get public key", "err", err)
+ return err
+ }
+
+ hasAssociatedAuthCoreCredential := false
+ var authCredential *domain.Claim
+ switch keyType {
+ case kms.KeyTypeBabyJubJub:
+ hasAssociatedAuthCoreCredential, authCredential, err = ks.hasAssociatedAuthCredential(ctx, did, publicKey)
+ if err != nil {
+ log.Error(ctx, "failed to check if key has associated auth credential", "err", err)
+ return err
+ }
+
+ if hasAssociatedAuthCoreCredential {
+ log.Info(ctx, "can not be deleted because it has an associated auth credential. Have to check revocation status")
+ revStatus, err := ks.claimService.GetRevocationStatus(ctx, *did, uint64(authCredential.RevNonce))
+ if err != nil {
+ log.Error(ctx, "failed to get revocation status", "err", err)
+ return err
+ }
+
+ if revStatus != nil && !revStatus.MTP.Existence {
+ log.Info(ctx, "auth credential is non revoked. Can not be deleted")
+ return ErrAuthCredentialNotRevoked
+ }
+ }
+ case kms.KeyTypeEthereum:
+ hasAssociatedAuthCoreCredential, err = ks.isAssociatedWithIdentity(ctx, did, publicKey)
+ if err != nil {
+ log.Error(ctx, "failed to check if key has associated auth credential", "err", err)
+ return err
+ }
+ if hasAssociatedAuthCoreCredential {
+ log.Info(ctx, "can not be deleted because it is associated with the identity")
+ return ErrKeyAssociatedWithIdentity
+ }
+ default:
+ return ErrInvalidKeyType
+ }
+
+ if err := ks.keyRepository.Delete(ctx, *did, hexutil.Encode(publicKey)); err != nil {
+ log.Error(ctx, "failed to delete key", "err", err)
+ return err
+ }
+ return ks.kms.Delete(ctx, kmsKeyID)
+}
+
+// getPublicKey returns the public key for the given keyID
+func (ks *Key) getPublicKey(ctx context.Context, keyID string) ([]byte, error) {
+ keyType, err := getKeyType(keyID)
+ if err != nil {
+ log.Error(ctx, "failed to get key type", "err", err)
+ return nil, err
+ }
+ kmsKeyID := kms.KeyID{
+ ID: keyID,
+ Type: keyType,
+ }
+
+ return ks.kms.PublicKey(kmsKeyID)
+}
+
+// getKeyType returns the key type for the given keyID
+func getKeyType(keyID string) (kms.KeyType, error) {
+ var keyType kms.KeyType
+ if strings.Contains(keyID, "BJJ") {
+ keyType = kms.KeyTypeBabyJubJub
+ } else if strings.Contains(keyID, "ETH") {
+ keyType = kms.KeyTypeEthereum
+ } else {
+ return keyType, ErrInvalidKeyType
+ }
+
+ return keyType, nil
+}
+
+// hasAssociatedAuthCredential checks if the bbj key has an associated auth credential
+func (ks *Key) hasAssociatedAuthCredential(ctx context.Context, did *w3c.DID, publicKey []byte) (bool, *domain.Claim, error) {
+ hasAssociatedAuthCredential := false
+ authCredential, err := ks.claimService.GetAuthCredentialByPublicKey(ctx, did, publicKey)
+ if err != nil {
+ log.Error(ctx, "failed to check if key has associated auth credential", "err", err)
+ return false, nil, err
+ }
+ hasAssociatedAuthCredential = authCredential != nil
+ return hasAssociatedAuthCredential, authCredential, nil
+}
+
+// isAssociatedWithIdentity checks if the eth key is associated with the identity
+func (ks *Key) isAssociatedWithIdentity(ctx context.Context, did *w3c.DID, publicKey []byte) (bool, error) {
+ hasAssociatedAuthCredential := false
+ pubKey, err := crypto.DecompressPubkey(publicKey)
+ if err != nil {
+ log.Error(ctx, "failed to decompress public key", "err", err)
+ return false, err
+ }
+
+ keyETHAddress := crypto.PubkeyToAddress(*pubKey)
+ isEthAddress, identityAddress, err := common.CheckEthIdentityByDID(did)
+ if err != nil {
+ log.Error(ctx, "failed to check if DID is ETH identity", "err", err)
+ return false, err
+ }
+
+ identityAddressToBeChecked := strings.ToUpper("0x" + identityAddress)
+ if isEthAddress {
+ hasAssociatedAuthCredential = identityAddressToBeChecked == strings.ToUpper(keyETHAddress.Hex())
+ }
+
+ return hasAssociatedAuthCredential, nil
+}
diff --git a/internal/core/services/link_test.go b/internal/core/services/link_test.go
index 4730975d2..1550e61fd 100644
--- a/internal/core/services/link_test.go
+++ b/internal/core/services/link_test.go
@@ -35,6 +35,7 @@ func Test_link_issueClaim(t *testing.T) {
schemaRepository := repositories.NewSchema(*storage)
mtService := NewIdentityMerkleTrees(mtRepo)
connectionsRepository := repositories.NewConnection()
+ keyRepository := repositories.NewKey(*storage)
reader := common.CreateFile(t)
networkResolver, err := networkPkg.NewResolver(ctx, cfg, keyStore, reader)
@@ -42,7 +43,7 @@ func Test_link_issueClaim(t *testing.T) {
rhsFactory := reversehash.NewFactory(*networkResolver, reversehash.DefaultRHSTimeOut)
revocationStatusResolver := revocationstatus.NewRevocationStatusResolver(*networkResolver)
- identityService := NewIdentity(keyStore, identityRepo, mtRepo, identityStateRepo, mtService, nil, claimsRepo, revocationRepository, connectionsRepository, storage, nil, nil, pubsub.NewMock(), *networkResolver, rhsFactory, revocationStatusResolver)
+ identityService := NewIdentity(keyStore, identityRepo, mtRepo, identityStateRepo, mtService, nil, claimsRepo, revocationRepository, connectionsRepository, storage, nil, nil, pubsub.NewMock(), *networkResolver, rhsFactory, revocationStatusResolver, keyRepository)
sessionRepository := repositories.NewSessionCached(cachex)
schemaService := NewSchema(schemaRepository, docLoader)
diff --git a/internal/core/services/notification_test.go b/internal/core/services/notification_test.go
index cc9c99a06..77abc02cc 100644
--- a/internal/core/services/notification_test.go
+++ b/internal/core/services/notification_test.go
@@ -39,6 +39,7 @@ func TestNotification_SendNotification(t *testing.T) {
mtService := NewIdentityMerkleTrees(mtRepo)
revocationRepository := repositories.NewRevocation()
connectionsRepository := repositories.NewConnection()
+ keyRepository := repositories.NewKey(*storage)
reader := common.CreateFile(t)
networkResolver, err := networkPkg.NewResolver(ctx, cfg, keyStore, reader)
@@ -46,7 +47,7 @@ func TestNotification_SendNotification(t *testing.T) {
rhsFactory := reversehash.NewFactory(*networkResolver, reversehash.DefaultRHSTimeOut)
revocationStatusResolver := revocationstatus.NewRevocationStatusResolver(*networkResolver)
- identityService := NewIdentity(keyStore, identityRepo, mtRepo, identityStateRepo, mtService, nil, claimsRepo, revocationRepository, connectionsRepository, storage, nil, nil, pubsub.NewMock(), *networkResolver, rhsFactory, revocationStatusResolver)
+ identityService := NewIdentity(keyStore, identityRepo, mtRepo, identityStateRepo, mtService, nil, claimsRepo, revocationRepository, connectionsRepository, storage, nil, nil, pubsub.NewMock(), *networkResolver, rhsFactory, revocationStatusResolver, keyRepository)
mediaTypeManager := NewMediaTypeManager(
map[iden3comm.ProtocolMessage][]string{
diff --git a/internal/db/schema/migrations/202412101722170_add_keys_table.sql b/internal/db/schema/migrations/202412101722170_add_keys_table.sql
new file mode 100644
index 000000000..faf15b958
--- /dev/null
+++ b/internal/db/schema/migrations/202412101722170_add_keys_table.sql
@@ -0,0 +1,18 @@
+-- +goose Up
+-- +goose StatementBegin
+CREATE TABLE keys(
+ id UUID PRIMARY KEY NOT NULL,
+ issuer_did text NOT NULL,
+ name text NOT NULL,
+ public_key text NOT NULL,
+ created_at timestamptz NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at timestamptz NULL DEFAULT CURRENT_TIMESTAMP,
+ CONSTRAINT keys_unique_name UNIQUE (issuer_did, name),
+ CONSTRAINT keys_identities_id_key foreign key (issuer_did) references identities (identifier)
+);
+-- +goose StatementEnd
+
+-- +goose Down
+-- +goose StatementBegin
+DROP TABLE IF EXISTS keys;
+-- +goose StatementEnd
\ No newline at end of file
diff --git a/internal/db/schema/migrations/202411131724000_payment_options.sql b/internal/db/schema/migrations/202412231508172_payment_options.sql
similarity index 100%
rename from internal/db/schema/migrations/202411131724000_payment_options.sql
rename to internal/db/schema/migrations/202412231508172_payment_options.sql
diff --git a/internal/kms/aws_kms_eth_key_provider.go b/internal/kms/aws_kms_eth_key_provider.go
index 3494c162f..39972e235 100644
--- a/internal/kms/aws_kms_eth_key_provider.go
+++ b/internal/kms/aws_kms_eth_key_provider.go
@@ -2,7 +2,6 @@ package kms
import (
"context"
- "encoding/hex"
"fmt"
"regexp"
"strings"
@@ -18,7 +17,11 @@ import (
"github.com/polygonid/sh-id-platform/internal/log"
)
-const aliasPrefix = "alias/"
+const (
+ aliasPrefix = "alias/"
+ awsKmdKeyIDPrefix = "ETH/"
+ awsKmsKeyIDParts = 2
+)
type awsKmsEthKeyProvider struct {
keyType KeyType
@@ -79,37 +82,26 @@ func (awsKeyProv *awsKmsEthKeyProvider) New(identity *w3c.DID) (KeyID, error) {
keyArn, err := awsKeyProv.kmsClient.CreateKey(ctx, input)
if err != nil {
- log.Error(ctx, "failed to create key: %v", err)
+ log.Error(ctx, "failed to create key", "err", err)
return KeyID{}, fmt.Errorf("failed to create key: %v", err)
}
- log.Info(ctx, "keyArn:", keyArn.KeyMetadata.Arn)
- inputPublicKey := &kms.GetPublicKeyInput{
- KeyId: aws.String(*keyArn.KeyMetadata.Arn),
- }
-
- publicKeyResult, err := awsKeyProv.kmsClient.GetPublicKey(ctx, inputPublicKey)
- if err != nil {
- return KeyID{}, fmt.Errorf("failed to get public key: %v", err)
- }
- pubKeyHex := hex.EncodeToString(publicKeyResult.PublicKey)
- keyID.ID = keyPathForAws(identity, awsKeyProv.keyType, pubKeyHex)
-
- aliasName := aliasPrefix + keyID.ID
- err = awsKeyProv.createAlias(ctx, aliasName, *keyArn.KeyMetadata.KeyId)
- if err != nil {
- log.Error(ctx, "failed to create alias: %v", err)
- return KeyID{}, fmt.Errorf("failed to create alias: %v", err)
- }
- keyID.ID = aliasName
+ log.Info(ctx, "keyArn", "keyArn", keyArn.KeyMetadata.Arn)
+ keyID.ID = awsKmdKeyIDPrefix + *keyArn.KeyMetadata.KeyId
return keyID, nil
}
// PublicKey returns public key for given keyID
func (awsKeyProv *awsKmsEthKeyProvider) PublicKey(keyID KeyID) ([]byte, error) {
+ ctx := context.Background()
if keyID.ID == awsKeyProv.issuerETHTransferKeyPath {
keyID.ID = aliasPrefix + awsKeyProv.issuerETHTransferKeyPath
+ } else {
+ keyIDParts := strings.Split(keyID.ID, "ETH/")
+ if len(keyIDParts) == awsKmsKeyIDParts {
+ keyID.ID = keyIDParts[1]
+ }
}
- ctx := context.Background()
+
inputPublicKey := &kms.GetPublicKeyInput{
KeyId: aws.String(keyID.ID),
}
@@ -124,7 +116,14 @@ func (awsKeyProv *awsKmsEthKeyProvider) PublicKey(keyID KeyID) ([]byte, error) {
func (awsKeyProv *awsKmsEthKeyProvider) Sign(ctx context.Context, keyID KeyID, data []byte) ([]byte, error) {
if keyID.ID == awsKeyProv.issuerETHTransferKeyPath {
keyID.ID = aliasPrefix + awsKeyProv.issuerETHTransferKeyPath
+ } else {
+ keyIDParts := strings.Split(keyID.ID, awsKmdKeyIDPrefix)
+ if len(keyIDParts) != awsKmsKeyIDParts {
+ return nil, fmt.Errorf("invalid keyID: %v", keyID.ID)
+ }
+ keyID.ID = keyIDParts[1]
}
+
signInput := &kms.SignInput{
KeyId: aws.String(keyID.ID),
Message: data,
@@ -165,14 +164,13 @@ func (awsKeyProv *awsKmsEthKeyProvider) Sign(ctx context.Context, keyID KeyID, d
// LinkToIdentity links key to identity
func (awsKeyProv *awsKmsEthKeyProvider) LinkToIdentity(ctx context.Context, keyID KeyID, identity w3c.DID) (KeyID, error) {
- keyMetadata, err := awsKeyProv.getKeyInfoByAlias(ctx, keyID.ID)
+ keyIDStr, err := getAwsKmsKeyID(keyID)
if err != nil {
- log.Error(ctx, "failed to get key metadata", "keyMetadata", keyMetadata, "err", err)
- return KeyID{}, fmt.Errorf("failed to get key metadata: %v", err)
+ return KeyID{}, err
}
tagResourceInput := &kms.TagResourceInput{
- KeyId: keyMetadata.KeyId,
+ KeyId: aws.String(keyIDStr),
Tags: []types.Tag{
{
TagKey: aws.String("keyType"),
@@ -187,18 +185,20 @@ func (awsKeyProv *awsKmsEthKeyProvider) LinkToIdentity(ctx context.Context, keyI
resourceOutput, err := awsKeyProv.kmsClient.TagResource(ctx, tagResourceInput)
if err != nil {
- log.Error(ctx, "failed to tag resource: %v", err)
+ log.Error(ctx, "failed to tag resource", "err", err)
return KeyID{}, fmt.Errorf("failed to tag resource: %v", err)
}
log.Info(ctx, "resource tagged:", "resourceOutput:", resourceOutput.ResultMetadata)
- keyID.ID = identity.String()
return keyID, nil
}
// ListByIdentity returns list of keyIDs for given identity
func (awsKeyProv *awsKmsEthKeyProvider) ListByIdentity(ctx context.Context, identity w3c.DID) ([]KeyID, error) {
- listKeysInput := &kms.ListKeysInput{}
+ const limit = 500
+ listKeysInput := &kms.ListKeysInput{
+ Limit: aws.Int32(limit),
+ }
listKeysOutput, err := awsKeyProv.kmsClient.ListKeys(ctx, listKeysInput)
if err != nil {
return nil, fmt.Errorf("failed to list keys: %w", err)
@@ -217,9 +217,10 @@ func (awsKeyProv *awsKmsEthKeyProvider) ListByIdentity(ctx context.Context, iden
for _, tag := range tagOutput.Tags {
if aws.ToString(tag.TagKey) == "did" && aws.ToString(tag.TagValue) == identity.String() {
+ id := "ETH/" + *key.KeyId
keysToReturn = append(keysToReturn, KeyID{
Type: KeyTypeEthereum,
- ID: aws.ToString(key.KeyId),
+ ID: aws.ToString(&id),
})
}
}
@@ -228,25 +229,41 @@ func (awsKeyProv *awsKmsEthKeyProvider) ListByIdentity(ctx context.Context, iden
return keysToReturn, nil
}
-// createAlias creates alias for key
-func (awsKeyProv *awsKmsEthKeyProvider) createAlias(ctx context.Context, aliasName, targetKeyId string) error {
- input := &kms.CreateAliasInput{
- AliasName: aws.String(aliasName),
- TargetKeyId: aws.String(targetKeyId),
+// Delete deletes key by keyID
+func (awsKeyProv *awsKmsEthKeyProvider) Delete(ctx context.Context, keyID KeyID) error {
+ const pendingWindowInDays = 7
+ keyIDStr, err := getAwsKmsKeyID(keyID)
+ if err != nil {
+ return err
}
- _, err := awsKeyProv.kmsClient.CreateAlias(ctx, input)
+ _, err = awsKeyProv.kmsClient.ScheduleKeyDeletion(ctx, &kms.ScheduleKeyDeletionInput{
+ KeyId: aws.String(keyIDStr),
+ PendingWindowInDays: aws.Int32(pendingWindowInDays),
+ })
+ return err
+}
+
+// Exists checks if key exists
+func (awsKeyProv *awsKmsEthKeyProvider) Exists(ctx context.Context, keyID KeyID) (bool, error) {
+ keyIDStr, err := getAwsKmsKeyID(keyID)
if err != nil {
- return fmt.Errorf("failed to create alias: %v", err)
+ return false, err
+ }
+ keyInfo, err := awsKeyProv.getKeyInfo(ctx, keyIDStr)
+ if err != nil {
+ return false, nil
}
- log.Info(ctx, "alias created:", "aliasName:", aliasName)
- return nil
+ if keyInfo != nil && keyInfo.DeletionDate != nil {
+ return false, nil
+ }
+ return true, nil
}
-// getKeyInfoByAlias returns key metadata by alias
-func (awsKeyProv *awsKmsEthKeyProvider) getKeyInfoByAlias(ctx context.Context, aliasName string) (*types.KeyMetadata, error) {
+// getKeyInfo returns key metadata by key id
+func (awsKeyProv *awsKmsEthKeyProvider) getKeyInfo(ctx context.Context, keyID string) (*types.KeyMetadata, error) {
aliasInput := &kms.DescribeKeyInput{
- KeyId: aws.String(aliasName),
+ KeyId: aws.String(keyID),
}
aliasOutput, err := awsKeyProv.kmsClient.DescribeKey(ctx, aliasInput)
if err != nil {
@@ -255,3 +272,11 @@ func (awsKeyProv *awsKmsEthKeyProvider) getKeyInfoByAlias(ctx context.Context, a
}
return aliasOutput.KeyMetadata, nil
}
+
+func getAwsKmsKeyID(keyID KeyID) (string, error) {
+ keyIDParts := strings.Split(keyID.ID, awsKmdKeyIDPrefix)
+ if len(keyIDParts) != awsKmsKeyIDParts {
+ return "", fmt.Errorf("invalid keyID: %v", keyID.ID)
+ }
+ return keyIDParts[1], nil
+}
diff --git a/internal/kms/aws_kms_eth_key_provider_test.go b/internal/kms/aws_kms_eth_key_provider_test.go
index ac55ecc10..c4c4fbcf9 100644
--- a/internal/kms/aws_kms_eth_key_provider_test.go
+++ b/internal/kms/aws_kms_eth_key_provider_test.go
@@ -71,12 +71,10 @@ func Test_LinkToIdentityInAWSKMS(t *testing.T) {
assert.NoError(t, err)
assert.NotEmpty(t, keyID.ID)
assert.Equal(t, ethereum, string(keyID.Type))
-
identity := randomDID(t)
-
- keyID, err = awsStorageProvider.LinkToIdentity(ctx, keyID, identity)
+ newKeyID, err := awsStorageProvider.LinkToIdentity(ctx, keyID, identity)
assert.NoError(t, err)
- assert.Equal(t, identity.String(), keyID.ID)
+ assert.Equal(t, keyID.ID, newKeyID.ID)
})
t.Run("should get an error", func(t *testing.T) {
diff --git a/internal/kms/aws_secret_storage_provider.go b/internal/kms/aws_secret_storage_provider.go
index 57bae4e1a..9cb6cbecd 100644
--- a/internal/kms/aws_secret_storage_provider.go
+++ b/internal/kms/aws_secret_storage_provider.go
@@ -166,3 +166,38 @@ func (a *awsSecretStorageProvider) searchPrivateKey(ctx context.Context, keyID K
}
return secretValue.PrivateKey, nil
}
+
+func (a *awsSecretStorageProvider) deleteKeyMaterial(ctx context.Context, keyID KeyID) error {
+ encodedSecretName := base64.StdEncoding.EncodeToString([]byte(keyID.ID))
+ input := &secretsmanager.DeleteSecretInput{
+ SecretId: aws.String(encodedSecretName),
+ ForceDeleteWithoutRecovery: aws.Bool(true),
+ }
+ _, err := a.secretManager.DeleteSecret(ctx, input)
+ return err
+}
+
+func (a *awsSecretStorageProvider) getKeyMaterial(ctx context.Context, keyID KeyID) (map[string]string, error) {
+ encodedSecretName := base64.StdEncoding.EncodeToString([]byte(keyID.ID))
+ input := &secretsmanager.GetSecretValueInput{
+ SecretId: aws.String(encodedSecretName),
+ }
+ result, err := a.secretManager.GetSecretValue(ctx, input)
+ if err != nil {
+ log.Error(ctx, "error getting secret value", "err", err)
+ if strings.Contains(err.Error(), "ResourceNotFoundException") {
+ return nil, ErrKeyNotFound
+ }
+ return nil, errors.New("error getting secret value from AWS")
+ }
+
+ var secretValue secretStorageProviderKeyMaterial
+ if err := json.Unmarshal([]byte(aws.ToString(result.SecretString)), &secretValue); err != nil {
+ return nil, err
+ }
+
+ return map[string]string{
+ jsonKeyType: secretValue.KeyType,
+ jsonKeyData: secretValue.PrivateKey,
+ }, nil
+}
diff --git a/internal/kms/aws_secret_storage_provider_test.go b/internal/kms/aws_secret_storage_provider_test.go
index 09db2e2b1..80941f0a0 100644
--- a/internal/kms/aws_secret_storage_provider_test.go
+++ b/internal/kms/aws_secret_storage_provider_test.go
@@ -221,3 +221,88 @@ func Test_searchPrivateKey(t *testing.T) {
assert.Equal(t, privateKey, privateKeyFromStore)
})
}
+
+func Test_getKeyMaterial(t *testing.T) {
+ ctx := context.Background()
+ awsStorageProvider, err := NewAwsSecretStorageProvider(ctx, AwsSecretStorageProviderConfig{
+ AccessKey: "access_key",
+ SecretKey: "secret_key",
+ Region: "local",
+ URL: "http://localhost:4566",
+ })
+ require.NoError(t, err)
+
+ t.Run("should get key material for bjj", func(t *testing.T) {
+ did := randomDID(t)
+ privateKey := "9d7abdd5a43573ab9b623c50b9fc8f4357329d3009fe0fc22c8931161d98a03d"
+ id := getKeyID(&did, KeyTypeBabyJubJub, "BJJ:2290140c920a31a596937095f18a9ae15c1fe7091091be485f353968a4310380")
+ err := awsStorageProvider.SaveKeyMaterial(ctx, map[string]string{
+ jsonKeyType: string(KeyTypeBabyJubJub),
+ jsonKeyData: privateKey,
+ }, id)
+ assert.NoError(t, err)
+
+ keyMaterial, err := awsStorageProvider.getKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: id,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, map[string]string{
+ jsonKeyType: string(babyjubjub),
+ jsonKeyData: privateKey,
+ }, keyMaterial)
+ })
+
+ t.Run("should get an error for bjj", func(t *testing.T) {
+ _, err := awsStorageProvider.getKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: "wrong_id",
+ })
+ require.Error(t, err)
+ })
+}
+
+func Test_deleteKeyMaterial(t *testing.T) {
+ ctx := context.Background()
+ awsStorageProvider, err := NewAwsSecretStorageProvider(ctx, AwsSecretStorageProviderConfig{
+ AccessKey: "access_key",
+ SecretKey: "secret_key",
+ Region: "local",
+ URL: "http://localhost:4566",
+ })
+ require.NoError(t, err)
+
+ t.Run("should delete key material for bjj", func(t *testing.T) {
+ did := randomDID(t)
+ privateKey := "9d7abdd5a43573ab9b623c50b9fc8f4357329d3009fe0fc22c8931161d98a03d"
+ id := getKeyID(&did, KeyTypeBabyJubJub, "BJJ:2290140c920a31a596937095f18a9ae15c1fe7091091be485f353968a4310380")
+ err := awsStorageProvider.SaveKeyMaterial(ctx, map[string]string{
+ jsonKeyType: string(KeyTypeBabyJubJub),
+ jsonKeyData: privateKey,
+ }, id)
+ assert.NoError(t, err)
+
+ keyMaterial, err := awsStorageProvider.getKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: id,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, map[string]string{
+ jsonKeyType: string(babyjubjub),
+ jsonKeyData: privateKey,
+ }, keyMaterial)
+
+ err = awsStorageProvider.deleteKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: id,
+ })
+
+ require.NoError(t, err)
+
+ _, err = awsStorageProvider.getKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: id,
+ })
+ require.Error(t, err)
+ })
+}
diff --git a/internal/kms/file_storage_manager.go b/internal/kms/file_storage_manager.go
index 36643108b..b6d55ae03 100644
--- a/internal/kms/file_storage_manager.go
+++ b/internal/kms/file_storage_manager.go
@@ -109,3 +109,44 @@ func readContentFile(ctx context.Context, file string) ([]localStorageProviderFi
return localStorageFileContent, nil
}
+
+func (ls *fileStorageManager) deleteKeyMaterial(ctx context.Context, keyID KeyID) error {
+ localStorageFileContent, err := readContentFile(ctx, ls.file)
+ if err != nil {
+ return err
+ }
+ for i, keyMaterial := range localStorageFileContent {
+ if keyMaterial.KeyPath == keyID.ID {
+ localStorageFileContent = append(localStorageFileContent[:i], localStorageFileContent[i+1:]...)
+ break
+ }
+ }
+
+ newFileContent, err := json.Marshal(localStorageFileContent)
+ if err != nil {
+ log.Error(ctx, "cannot marshal file content", "err", err)
+ return err
+ }
+ // nolint: all
+ if err := os.WriteFile(ls.file, newFileContent, 0644); err != nil {
+ log.Error(ctx, "cannot write file", "err", err)
+ return err
+ }
+ return nil
+}
+
+func (ls *fileStorageManager) getKeyMaterial(ctx context.Context, keyID KeyID) (map[string]string, error) {
+ localStorageFileContent, err := readContentFile(ctx, ls.file)
+ if err != nil {
+ return nil, err
+ }
+ for _, keyMaterial := range localStorageFileContent {
+ if keyMaterial.KeyPath == keyID.ID {
+ return map[string]string{
+ jsonKeyType: keyMaterial.KeyType,
+ jsonKeyData: keyMaterial.PrivateKey,
+ }, nil
+ }
+ }
+ return nil, ErrKeyNotFound
+}
diff --git a/internal/kms/file_storage_manager_test.go b/internal/kms/file_storage_manager_test.go
index 1125f94bd..28fad8a8f 100644
--- a/internal/kms/file_storage_manager_test.go
+++ b/internal/kms/file_storage_manager_test.go
@@ -164,6 +164,91 @@ func TestSearchPrivateKeyInFile_ReturnsErrorWhenKeyNotFound(t *testing.T) {
assert.Error(t, err)
}
+func Test_GetKeyMaterial(t *testing.T) {
+ tmpFile, err := createTestFile(t)
+ assert.NoError(t, err)
+ //nolint:errcheck
+ defer os.Remove(tmpFile.Name())
+
+ ls := NewFileStorageManager(tmpFile.Name())
+ ctx := context.Background()
+
+ t.Run("should return key material", func(t *testing.T) {
+ did := randomDID(t)
+ privateKey := "9d7abdd5a43573ab9b623c50b9fc8f4357329d3009fe0fc22c8931161d98a03d"
+ id := getKeyID(&did, KeyTypeBabyJubJub, "BJJ:2290140c920a31a596937095f18a9ae15c1fe7091091be485f353968a4310380")
+
+ err = ls.SaveKeyMaterial(ctx, map[string]string{
+ jsonKeyType: string(KeyTypeBabyJubJub),
+ jsonKeyData: privateKey,
+ }, id)
+ assert.NoError(t, err)
+
+ keyMaterial, err := ls.getKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: id,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, map[string]string{
+ jsonKeyType: string(babyjubjub),
+ jsonKeyData: privateKey,
+ }, keyMaterial)
+ })
+
+ t.Run("should return an error", func(t *testing.T) {
+ _, err := ls.getKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: "wrong_id",
+ })
+ require.Error(t, err)
+ })
+}
+
+func Test_DeleteKeyMaterial(t *testing.T) {
+ tmpFile, err := createTestFile(t)
+ require.NoError(t, err)
+ //nolint:errcheck
+ defer os.Remove(tmpFile.Name())
+
+ ls := NewFileStorageManager(tmpFile.Name())
+ ctx := context.Background()
+
+ t.Run("should delete key material", func(t *testing.T) {
+ did := randomDID(t)
+ privateKey := "9d7abdd5a43573ab9b623c50b9fc8f4357329d3009fe0fc22c8931161d98a03d"
+ id := getKeyID(&did, KeyTypeBabyJubJub, "BJJ:2290140c920a31a596937095f18a9ae15c1fe7091091be485f353968a4310380")
+
+ err = ls.SaveKeyMaterial(ctx, map[string]string{
+ jsonKeyType: string(KeyTypeBabyJubJub),
+ jsonKeyData: privateKey,
+ }, id)
+ assert.NoError(t, err)
+
+ keyMaterial, err := ls.getKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: id,
+ })
+ require.NoError(t, err)
+ assert.Equal(t, map[string]string{
+ jsonKeyType: string(babyjubjub),
+ jsonKeyData: privateKey,
+ }, keyMaterial)
+
+ err = ls.deleteKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: id,
+ })
+
+ require.NoError(t, err)
+
+ _, err = ls.getKeyMaterial(ctx, KeyID{
+ Type: babyjubjub,
+ ID: id,
+ })
+ require.Error(t, err)
+ })
+}
+
func createTestFile(t *testing.T) (*os.File, error) {
t.Helper()
tmpFile, err := os.Create("./kms.json")
diff --git a/internal/kms/kms.go b/internal/kms/kms.go
index e3b7d2e57..2b921df46 100644
--- a/internal/kms/kms.go
+++ b/internal/kms/kms.go
@@ -20,6 +20,8 @@ type StorageManager interface {
SaveKeyMaterial(ctx context.Context, keyMaterial map[string]string, id string) error
searchByIdentity(ctx context.Context, identity w3c.DID, keyType KeyType) ([]KeyID, error)
searchPrivateKey(ctx context.Context, keyID KeyID) (string, error)
+ deleteKeyMaterial(ctx context.Context, keyID KeyID) error
+ getKeyMaterial(ctx context.Context, keyID KeyID) (map[string]string, error)
}
// KMSType represents the KMS interface
@@ -31,6 +33,8 @@ type KMSType interface {
Sign(ctx context.Context, keyID KeyID, data []byte) ([]byte, error)
KeysByIdentity(ctx context.Context, identity w3c.DID) ([]KeyID, error)
LinkToIdentity(ctx context.Context, keyID KeyID, identity w3c.DID) (KeyID, error)
+ Delete(ctx context.Context, keyID KeyID) error
+ Exists(ctx context.Context, keyID KeyID) (bool, error)
}
// ConfigProvider is a key provider configuration
@@ -82,6 +86,10 @@ type KeyProvider interface {
// KeyID can be changed after linking.
// Returning new KeyID.
LinkToIdentity(ctx context.Context, keyID KeyID, identity w3c.DID) (KeyID, error)
+ // Delete removes key from storage
+ Delete(ctx context.Context, keyID KeyID) error
+ // Exists checks if key exists
+ Exists(ctx context.Context, keyID KeyID) (bool, error)
}
// KMS stores keys and secrets
@@ -111,6 +119,9 @@ var ErrKeyTypeConflict = stderr.New("key type already registered")
// ErrPermissionDenied raises when we register new key provider with key type
var ErrPermissionDenied = stderr.New("permission denied")
+// ErrKeyNotFound raises when key is not found
+var ErrKeyNotFound = stderr.New("key not found")
+
// KeyID is a key unique identifier
type KeyID struct {
Type KeyType
@@ -222,6 +233,24 @@ func (k *KMS) LinkToIdentity(ctx context.Context, keyID KeyID, identity w3c.DID)
return kp.LinkToIdentity(ctx, keyID, identity)
}
+// Delete removes key from storage
+func (k *KMS) Delete(ctx context.Context, keyID KeyID) error {
+ kp, ok := k.registry[keyID.Type]
+ if !ok {
+ return errors.WithStack(ErrUnknownKeyType)
+ }
+ return kp.Delete(ctx, keyID)
+}
+
+// Exists checks if key exists
+func (k *KMS) Exists(ctx context.Context, keyID KeyID) (bool, error) {
+ kp, ok := k.registry[keyID.Type]
+ if !ok {
+ return false, errors.WithStack(ErrUnknownKeyType)
+ }
+ return kp.Exists(ctx, keyID)
+}
+
// Open returns an initialized KMS
func Open(pluginIden3MountPath string, vault *api.Client) (*KMS, error) {
bjjKeyProvider, err := NewVaultPluginIden3KeyProvider(vault, pluginIden3MountPath, KeyTypeBabyJubJub)
diff --git a/internal/kms/local_bjj_key_provider.go b/internal/kms/local_bjj_key_provider.go
index fc6e0ad00..728a2d126 100644
--- a/internal/kms/local_bjj_key_provider.go
+++ b/internal/kms/local_bjj_key_provider.go
@@ -125,6 +125,10 @@ func (ls *localBJJKeyProvider) LinkToIdentity(ctx context.Context, keyID KeyID,
return keyID, nil
}
+func (ls *localBJJKeyProvider) Delete(ctx context.Context, keyID KeyID) error {
+ return ls.storageManager.deleteKeyMaterial(ctx, keyID)
+}
+
func (ls *localBJJKeyProvider) privateKey(ctx context.Context, keyID KeyID) ([]byte, error) {
if keyID.Type != ls.keyType {
return nil, ErrIncorrectKeyType
@@ -155,3 +159,13 @@ func (ls *localBJJKeyProvider) privateKey(ctx context.Context, keyID KeyID) ([]b
return val, nil
}
+
+func (ls *localBJJKeyProvider) Exists(ctx context.Context, keyID KeyID) (bool, error) {
+ _, err := ls.storageManager.getKeyMaterial(ctx, keyID)
+ if err != nil {
+ if errors.Is(err, ErrKeyNotFound) {
+ return false, nil
+ }
+ }
+ return true, nil
+}
diff --git a/internal/kms/local_bjj_key_provider_test.go b/internal/kms/local_bjj_key_provider_test.go
index bf0de78a7..db378dba0 100644
--- a/internal/kms/local_bjj_key_provider_test.go
+++ b/internal/kms/local_bjj_key_provider_test.go
@@ -259,12 +259,14 @@ func Test_PublicKey_LocalBJJKeyProvider(t *testing.T) {
AccessKey: "access_key",
SecretKey: "secret_key",
Region: "local",
+ URL: "http://localhost:4566",
})
require.NoError(t, err)
t.Run("should get public key using local storage manager", func(t *testing.T) {
+ did := randomDID(t)
localbbjKeyProvider := NewLocalBJJKeyProvider(KeyTypeBabyJubJub, ls)
- keyID, err := localbbjKeyProvider.New(nil)
+ keyID, err := localbbjKeyProvider.New(&did)
assert.NoError(t, err)
assert.NotEmpty(t, keyID.ID)
@@ -274,8 +276,9 @@ func Test_PublicKey_LocalBJJKeyProvider(t *testing.T) {
})
t.Run("should get public key using aws storage manager", func(t *testing.T) {
+ did := randomDID(t)
localbbjKeyProvider := NewLocalBJJKeyProvider(KeyTypeBabyJubJub, awsStorageProvider)
- keyID, err := localbbjKeyProvider.New(nil)
+ keyID, err := localbbjKeyProvider.New(&did)
assert.NoError(t, err)
assert.NotEmpty(t, keyID.ID)
@@ -375,3 +378,42 @@ func Test_Sign_LocalBJJKeyProvider(t *testing.T) {
assert.NotNil(t, signature)
})
}
+
+func Test_DeleteKey_LocalBJJKeyProvider(t *testing.T) {
+ ctx := context.Background()
+ tmpFile, err := createTestFile(t)
+ assert.NoError(t, err)
+ //nolint:errcheck
+ defer os.Remove(tmpFile.Name())
+ ls := NewFileStorageManager(tmpFile.Name())
+
+ awsStorageProvider, err := NewAwsSecretStorageProvider(ctx, AwsSecretStorageProviderConfig{
+ AccessKey: "access_key",
+ SecretKey: "secret_key",
+ Region: "local",
+ URL: "http://localhost:4566",
+ })
+ require.NoError(t, err)
+
+ t.Run("should get public key using local storage manager", func(t *testing.T) {
+ did := randomDID(t)
+ localbbjKeyProvider := NewLocalBJJKeyProvider(KeyTypeBabyJubJub, ls)
+ keyID, err := localbbjKeyProvider.New(&did)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, keyID.ID)
+
+ err = localbbjKeyProvider.Delete(ctx, keyID)
+ assert.NoError(t, err)
+ })
+
+ t.Run("should get public key using aws storage manager", func(t *testing.T) {
+ did := randomDID(t)
+ localbbjKeyProvider := NewLocalBJJKeyProvider(KeyTypeBabyJubJub, awsStorageProvider)
+ keyID, err := localbbjKeyProvider.New(&did)
+ assert.NoError(t, err)
+ assert.NotEmpty(t, keyID.ID)
+
+ err = localbbjKeyProvider.Delete(ctx, keyID)
+ assert.NoError(t, err)
+ })
+}
diff --git a/internal/kms/local_eth_key_provider.go b/internal/kms/local_eth_key_provider.go
index b384bbf02..b6bea84cc 100644
--- a/internal/kms/local_eth_key_provider.go
+++ b/internal/kms/local_eth_key_provider.go
@@ -119,7 +119,7 @@ func (ls *localEthKeyProvider) LinkToIdentity(ctx context.Context, keyID KeyID,
return KeyID{}, err
}
- keyID.ID = identity.String()
+ keyID.ID = identity.String() + "/" + keyID.ID
return keyID, nil
}
@@ -128,6 +128,20 @@ func (ls *localEthKeyProvider) ListByIdentity(ctx context.Context, identity w3c.
return ls.storageManager.searchByIdentity(ctx, identity, ls.keyType)
}
+func (ls *localEthKeyProvider) Delete(ctx context.Context, keyID KeyID) error {
+ return ls.storageManager.deleteKeyMaterial(ctx, keyID)
+}
+
+func (ls *localEthKeyProvider) Exists(ctx context.Context, keyID KeyID) (bool, error) {
+ _, err := ls.storageManager.getKeyMaterial(ctx, keyID)
+ if err != nil {
+ if errors.Is(err, ErrKeyNotFound) {
+ return false, nil
+ }
+ }
+ return true, nil
+}
+
// nolint
func (ls *localEthKeyProvider) privateKey(ctx context.Context, keyID KeyID) ([]byte, error) {
if keyID.Type != ls.keyType {
diff --git a/internal/kms/local_eth_key_provider_test.go b/internal/kms/local_eth_key_provider_test.go
index db6b6e440..28b524526 100644
--- a/internal/kms/local_eth_key_provider_test.go
+++ b/internal/kms/local_eth_key_provider_test.go
@@ -96,10 +96,10 @@ func Test_LinkToIdentity_LocalETHKeyProvider(t *testing.T) {
assert.NotEmpty(t, keyID.ID)
did := randomDID(t)
- keyID, err = localETHKeyProvider.LinkToIdentity(ctx, keyID, did)
+ newKeyID, err := localETHKeyProvider.LinkToIdentity(ctx, keyID, did)
assert.NoError(t, err)
assert.NotNil(t, keyID)
- assert.Equal(t, did.String(), keyID.ID)
+ assert.Equal(t, did.String()+"/"+keyID.ID, newKeyID.ID)
assert.Equal(t, KeyTypeEthereum, keyID.Type)
})
@@ -110,10 +110,10 @@ func Test_LinkToIdentity_LocalETHKeyProvider(t *testing.T) {
assert.NotEmpty(t, keyID.ID)
did := randomDID(t)
- keyID, err = localETHKeyProvider.LinkToIdentity(ctx, keyID, did)
+ newKeyID, err := localETHKeyProvider.LinkToIdentity(ctx, keyID, did)
assert.NoError(t, err)
assert.NotNil(t, keyID)
- assert.Equal(t, did.String(), keyID.ID)
+ assert.Equal(t, did.String()+"/"+keyID.ID, newKeyID.ID)
assert.Equal(t, KeyTypeEthereum, keyID.Type)
})
}
diff --git a/internal/kms/vaultPluginIden3KeyProvider.go b/internal/kms/vaultPluginIden3KeyProvider.go
index f25ac181f..aea4b4b0f 100644
--- a/internal/kms/vaultPluginIden3KeyProvider.go
+++ b/internal/kms/vaultPluginIden3KeyProvider.go
@@ -128,6 +128,9 @@ func (v *vaultPluginIden3KeyProvider) PublicKey(keyID KeyID) ([]byte, error) {
publicKeyStr, err := publicKey(v.vaultCli, v.keyPathFromID(keyID))
if err != nil {
+ if strings.Contains(err.Error(), "secret is nil") {
+ return nil, ErrKeyNotFound
+ }
return nil, err
}
@@ -161,6 +164,22 @@ func (v *vaultPluginIden3KeyProvider) New(identity *w3c.DID) (KeyID, error) {
return keyID, nil
}
+func (v *vaultPluginIden3KeyProvider) Delete(ctx context.Context, keyID KeyID) error {
+ _, err := v.vaultCli.Logical().Delete(v.keyPathFromID(keyID).keys())
+ return err
+}
+
+func (v *vaultPluginIden3KeyProvider) Exists(ctx context.Context, keyID KeyID) (bool, error) {
+ _, err := publicKey(v.vaultCli, v.keyPathFromID(keyID))
+ if err != nil {
+ if strings.Contains(err.Error(), "secret is nil") {
+ return false, nil
+ }
+ return false, err
+ }
+ return true, nil
+}
+
func (v *vaultPluginIden3KeyProvider) randomKeyPath() (keyPathT, error) {
var rnd [16]byte
_, err := rand.Read(rnd[:])
diff --git a/internal/kms/vault_bjj_key_provider.go b/internal/kms/vault_bjj_key_provider.go
index c99a248ec..4ebdb28f2 100644
--- a/internal/kms/vault_bjj_key_provider.go
+++ b/internal/kms/vault_bjj_key_provider.go
@@ -134,6 +134,14 @@ func (v *vaultBJJKeyProvider) PublicKey(keyID KeyID) ([]byte, error) {
return val, err
}
+func (v *vaultBJJKeyProvider) Delete(ctx context.Context, keyID KeyID) error {
+ return nil
+}
+
+func (v *vaultBJJKeyProvider) Exists(ctx context.Context, keyID KeyID) (bool, error) {
+ return false, errors.New("not implemented")
+}
+
func (v *vaultBJJKeyProvider) privateKey(keyID KeyID) ([]byte, error) {
if keyID.Type != v.keyType {
return nil, ErrIncorrectKeyType
diff --git a/internal/kms/vault_eth_key_provider.go b/internal/kms/vault_eth_key_provider.go
index c9433dad3..535a67ec5 100644
--- a/internal/kms/vault_eth_key_provider.go
+++ b/internal/kms/vault_eth_key_provider.go
@@ -193,6 +193,15 @@ func (v *vaultETHKeyProvider) New(identity *w3c.DID) (KeyID, error) {
return keyID, saveKeyMaterial(v.vaultCli, keyID.ID, keyMaterial)
}
+func (v *vaultETHKeyProvider) Delete(ctx context.Context, keyID KeyID) error {
+ _, err := v.vaultCli.Logical().Delete(absVaultSecretPath(keyID.ID))
+ return err
+}
+
+func (v *vaultETHKeyProvider) Exists(ctx context.Context, keyID KeyID) (bool, error) {
+ return false, errors.New("not implemented")
+}
+
// NewVaultEthProvider creates new provider for Ethereum keys stored in vault
func NewVaultEthProvider(valutCli *api.Client, keyType KeyType) KeyProvider {
reIdenKeyPathHex := regexp.MustCompile("^(?i).*/" +
diff --git a/internal/kms/vault_providers_helpers.go b/internal/kms/vault_providers_helpers.go
index 6306cbee4..2073fc591 100644
--- a/internal/kms/vault_providers_helpers.go
+++ b/internal/kms/vault_providers_helpers.go
@@ -73,14 +73,6 @@ func keyPath(identity *w3c.DID, keyType KeyType, keyID string) string {
return basePath + string(keyType) + ":" + keyID
}
-func keyPathForAws(identity *w3c.DID, keyType KeyType, keyID string) string {
- basePath := ""
- if identity != nil {
- basePath = identityPath(identity) + "/"
- }
- return basePath + string(keyType) + keyID
-}
-
func absVaultSecretPath(path string) string {
return kvStoragePath + "/data/" + strings.TrimPrefix(path, "/")
}
diff --git a/internal/repositories/claim.go b/internal/repositories/claim.go
index 54cb6ffe6..0731b7a8c 100644
--- a/internal/repositories/claim.go
+++ b/internal/repositories/claim.go
@@ -24,8 +24,8 @@ import (
const duplicateViolationErrorCode = "23505"
-// ErrClaimDuplication claim duplication error
var (
+ // ErrClaimDuplication claim duplication error
ErrClaimDuplication = errors.New("claim duplication error")
// ErrClaimDoesNotExist claim does not exist
ErrClaimDoesNotExist = errors.New("claim does not exist")
@@ -345,6 +345,8 @@ func (c *claim) GetByRevocationNonce(ctx context.Context, conn db.Querier, ident
return claims, nil
}
+// FindOneClaimBySchemaHash returns a claim by schema hash
+// The claim must have MTP proof and not be revoked. This means the claim is published.
func (c *claim) FindOneClaimBySchemaHash(ctx context.Context, conn db.Querier, subject *w3c.DID, schemaHash string) (*domain.Claim, error) {
var claim domain.Claim
@@ -371,13 +373,14 @@ func (c *claim) FindOneClaimBySchemaHash(ctx context.Context, conn db.Querier, s
WHERE claims.identifier=$1
AND ( claims.other_identifier = $1 or claims.other_identifier = '')
AND claims.schema_hash = $2
- AND claims.revoked = false`, subject.String(), schemaHash)
+ AND claims.revoked = false
+ AND claims.mtp_proof IS NOT NULL `, subject.String(), schemaHash)
err := row.Scan(&claim.ID,
&claim.Issuer,
&claim.SchemaHash,
&claim.SchemaType,
- &claim.SchemaHash,
+ &claim.SchemaURL,
&claim.OtherIdentifier,
&claim.Expiration,
&claim.Updatable,
@@ -399,6 +402,73 @@ func (c *claim) FindOneClaimBySchemaHash(ctx context.Context, conn db.Querier, s
return &claim, err
}
+// FindClaimsBySchemaHash returns all claims by schema hash
+// The claim must have MTP proof and not be revoked.
+func (c *claim) FindClaimsBySchemaHash(ctx context.Context, conn db.Querier, subject *w3c.DID, schemaHash string) ([]*domain.Claim, error) {
+ rows, err := conn.Query(ctx,
+ `SELECT claims.id,
+ issuer,
+ schema_hash,
+ schema_type,
+ schema_url,
+ other_identifier,
+ expiration,
+ updatable,
+ claims.version,
+ rev_nonce,
+ mtp_proof,
+ signature_proof,
+ data,
+ claims.identifier,
+ identity_state,
+ credential_status,
+ core_claim,
+ revoked,
+ mtp,
+ claims.created_at
+ FROM claims
+ WHERE claims.identifier=$1
+ AND ( claims.other_identifier = $1 or claims.other_identifier = '')
+ AND claims.schema_hash = $2
+ AND claims.revoked = false
+ AND claims.mtp_proof IS NOT NULL `, subject.String(), schemaHash)
+ if err != nil {
+ return nil, err
+ }
+ defer rows.Close()
+
+ credentials := make([]*domain.Claim, 0)
+ for rows.Next() {
+ var claim domain.Claim
+ err := rows.Scan(&claim.ID,
+ &claim.Issuer,
+ &claim.SchemaHash,
+ &claim.SchemaType,
+ &claim.SchemaURL,
+ &claim.OtherIdentifier,
+ &claim.Expiration,
+ &claim.Updatable,
+ &claim.Version,
+ &claim.RevNonce,
+ &claim.MTPProof,
+ &claim.SignatureProof,
+ &claim.Data,
+ &claim.Identifier,
+ &claim.IdentityState,
+ &claim.CredentialStatus,
+ &claim.CoreClaim,
+ &claim.Revoked,
+ &claim.MtProof,
+ &claim.CreatedAt)
+ if err != nil {
+ return nil, err
+ }
+ credentials = append(credentials, &claim)
+ }
+
+ return credentials, nil
+}
+
func (c *claim) RevokeNonce(ctx context.Context, conn db.Querier, revocation *domain.Revocation) error {
_, err := conn.Exec(ctx,
` INSERT INTO revocation (identifier, nonce, version, status, description)
@@ -730,8 +800,8 @@ func processClaims(rows pgx.Rows) ([]*domain.Claim, error) {
err := rows.Scan(&claim.ID,
&claim.Issuer,
&claim.SchemaHash,
- &claim.SchemaURL,
&claim.SchemaType,
+ &claim.SchemaURL,
&claim.OtherIdentifier,
&claim.Expiration,
&claim.Updatable,
@@ -1098,3 +1168,62 @@ func (c *claim) GetByStateIDWithMTPProof(ctx context.Context, conn db.Querier, d
return claims, nil
}
+
+// GetAuthCoreClaims returns all the core claims for the given identifier and schema hash
+// The auth core claims returned may not be published onchain.
+func (c *claim) GetAuthCoreClaims(ctx context.Context, conn db.Querier, identifier *w3c.DID, schemaHash string) ([]*domain.Claim, error) {
+ rows, err := conn.Query(ctx,
+ `SELECT claims.id,
+ issuer,
+ schema_hash,
+ schema_type,
+ schema_url,
+ other_identifier,
+ expiration,
+ updatable,
+ claims.version,
+ rev_nonce,
+ mtp_proof,
+ signature_proof,
+ data,
+ claims.identifier,
+ identity_state,
+ credential_status,
+ revoked,
+ core_claim
+ FROM claims
+ WHERE claims.identifier=$1
+ AND ( claims.other_identifier = $1 or claims.other_identifier = '')
+ AND claims.schema_hash = $2`, identifier.String(), schemaHash)
+ if err != nil {
+ return nil, err
+ }
+
+ claims := make([]*domain.Claim, 0)
+ for rows.Next() {
+ var claim domain.Claim
+ err := rows.Scan(&claim.ID,
+ &claim.Issuer,
+ &claim.SchemaHash,
+ &claim.SchemaType,
+ &claim.SchemaURL,
+ &claim.OtherIdentifier,
+ &claim.Expiration,
+ &claim.Updatable,
+ &claim.Version,
+ &claim.RevNonce,
+ &claim.MTPProof,
+ &claim.SignatureProof,
+ &claim.Data,
+ &claim.Identifier,
+ &claim.IdentityState,
+ &claim.CredentialStatus,
+ &claim.Revoked,
+ &claim.CoreClaim)
+ if err != nil {
+ return nil, err
+ }
+ claims = append(claims, &claim)
+ }
+ return claims, nil
+}
diff --git a/internal/repositories/fixture.go b/internal/repositories/fixture.go
index 26a223b98..0a631d836 100644
--- a/internal/repositories/fixture.go
+++ b/internal/repositories/fixture.go
@@ -13,7 +13,7 @@ import (
// Fixture - Handle testing fixture configuration
type Fixture struct {
storage *db.Storage
- identityRepository ports.IndentityRepository
+ identityRepository ports.IdentityRepository
claimRepository ports.ClaimRepository
connectionsRepository ports.ConnectionRepository
schemaRepository ports.SchemaRepository
diff --git a/internal/repositories/identity.go b/internal/repositories/identity.go
index e139add6b..2504c5d2a 100644
--- a/internal/repositories/identity.go
+++ b/internal/repositories/identity.go
@@ -22,8 +22,8 @@ var ErrDisplayNameDuplicated = errors.New("display name already exists")
type identity struct{}
-// NewIdentity TODO
-func NewIdentity() ports.IndentityRepository {
+// NewIdentity - Create new identity repository
+func NewIdentity() ports.IdentityRepository {
return &identity{}
}
@@ -70,7 +70,8 @@ func (i *identity) GetByID(ctx context.Context, conn db.Querier, identifier w3c.
claims.credential_status
FROM identities
LEFT JOIN identity_states ON identities.identifier = identity_states.identifier
- LEFT JOIN claims ON claims.identifier = identities.identifier and claims.schema_type = 'https://schema.iden3.io/core/jsonld/auth.jsonld#AuthBJJCredential'
+ LEFT JOIN claims ON claims.identifier = identities.identifier
+ AND claims.schema_type = 'https://schema.iden3.io/core/jsonld/auth.jsonld#AuthBJJCredential'
WHERE identities.identifier=$1
AND ( status = 'transacted' OR status = 'confirmed')
OR (identities.identifier=$1 AND status = 'created' AND previous_state is null
diff --git a/internal/repositories/key.go b/internal/repositories/key.go
new file mode 100644
index 000000000..ae4e3284a
--- /dev/null
+++ b/internal/repositories/key.go
@@ -0,0 +1,95 @@
+package repositories
+
+import (
+ "context"
+ "errors"
+ "strings"
+
+ "github.com/google/uuid"
+ "github.com/iden3/go-iden3-core/v2/w3c"
+ "github.com/jackc/pgconn"
+
+ "github.com/polygonid/sh-id-platform/internal/core/domain"
+ "github.com/polygonid/sh-id-platform/internal/db"
+ "github.com/polygonid/sh-id-platform/internal/log"
+)
+
+var (
+ // ErrKeyNotFound key not found error
+ ErrKeyNotFound = errors.New("key not found")
+ // ErrDuplicateKeyName duplicate key name error
+ ErrDuplicateKeyName = errors.New("key name already exists")
+)
+
+type key struct {
+ conn db.Storage
+}
+
+// NewKey returns a new key repository
+func NewKey(conn db.Storage) *key {
+ return &key{
+ conn,
+ }
+}
+
+// Save saves a key
+func (k *key) Save(ctx context.Context, conn db.Querier, key *domain.Key) (uuid.UUID, error) {
+ if conn == nil {
+ conn = k.conn.Pgx
+ }
+ sql := `INSERT INTO keys (id, issuer_did, public_key, name)
+ VALUES($1, $2, $3, $4) ON CONFLICT (id) DO
+ UPDATE SET name=$4`
+ _, err := conn.Exec(ctx, sql, key.ID, key.IssuerCoreDID().String(), key.PublicKey, key.Name)
+ if err != nil {
+ var pgErr *pgconn.PgError
+ if errors.As(err, &pgErr) && pgErr.Code == duplicateViolationErrorCode {
+ return uuid.Nil, ErrDuplicateKeyName
+ }
+ return uuid.Nil, err
+ }
+ return key.ID, err
+}
+
+// GetByPublicKey returns a key by its public key
+func (k *key) GetByPublicKey(ctx context.Context, issuerDID w3c.DID, publicKey string) (*domain.Key, error) {
+ sql := `SELECT id, issuer_did, public_key, name
+ FROM keys WHERE issuer_did=$1 and public_key=$2`
+ row := k.conn.Pgx.QueryRow(ctx, sql, issuerDID.String(), publicKey)
+
+ key := domain.Key{}
+ err := row.Scan(&key.ID, &key.IssuerDID, &key.PublicKey, &key.Name)
+ if err != nil {
+ log.Error(ctx, "error getting key by public key", "err", err)
+ if strings.Contains(err.Error(), "no rows in result set") {
+ return nil, ErrKeyNotFound
+ }
+ return nil, err
+ }
+ return &key, nil
+}
+
+// Delete deletes a key by its public key
+func (k *key) Delete(ctx context.Context, issuerDID w3c.DID, publicKey string) error {
+ sql := `DELETE FROM keys WHERE issuer_did=$1 AND public_key=$2`
+ _, err := k.conn.Pgx.Exec(ctx, sql, issuerDID.String(), publicKey)
+ return err
+}
+
+// GetByName returns a key by its name
+func (k *key) GetByName(ctx context.Context, issuerDID w3c.DID, name string) (*domain.Key, error) {
+ sql := `SELECT id, issuer_did, public_key, name
+ FROM keys WHERE issuer_did=$1 and name=$2`
+ row := k.conn.Pgx.QueryRow(ctx, sql, issuerDID.String(), name)
+
+ key := domain.Key{}
+ err := row.Scan(&key.ID, &key.IssuerDID, &key.PublicKey, &key.Name)
+ if err != nil {
+ log.Error(ctx, "error getting key by name", "err", err)
+ if strings.Contains(err.Error(), "no rows in result set") {
+ return nil, ErrKeyNotFound
+ }
+ return nil, err
+ }
+ return &key, nil
+}
diff --git a/internal/repositories/key_test.go b/internal/repositories/key_test.go
new file mode 100644
index 000000000..54810a829
--- /dev/null
+++ b/internal/repositories/key_test.go
@@ -0,0 +1,95 @@
+package repositories
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/polygonid/sh-id-platform/internal/core/domain"
+)
+
+func TestKey_Save(t *testing.T) {
+ keyRepository := NewKey(*storage)
+
+ ctx := context.Background()
+ did := randomDID(t)
+ _, err := storage.Pgx.Exec(ctx, "INSERT INTO identities (identifier, keytype) VALUES ($1, $2)", did.String(), "BJJ")
+ assert.NoError(t, err)
+
+ t.Run("should save a new key", func(t *testing.T) {
+ key := domain.Key{
+ ID: uuid.New(),
+ IssuerDID: domain.KeyCoreDID(did),
+ PublicKey: "publicKey",
+ Name: "name",
+ }
+ id, err := keyRepository.Save(context.Background(), storage.Pgx, &key)
+ require.NoError(t, err)
+ require.NotEqual(t, uuid.Nil, id)
+ assert.Equal(t, key.ID, id)
+ })
+
+ t.Run("should get an error", func(t *testing.T) {
+ key := domain.Key{
+ ID: uuid.New(),
+ IssuerDID: domain.KeyCoreDID(did),
+ PublicKey: "publicKey",
+ Name: "name_1",
+ }
+
+ id, err := keyRepository.Save(context.Background(), storage.Pgx, &key)
+ require.NoError(t, err)
+ require.NotEqual(t, uuid.Nil, id)
+ assert.Equal(t, key.ID, id)
+
+ key2 := domain.Key{
+ ID: uuid.New(),
+ IssuerDID: domain.KeyCoreDID(did),
+ PublicKey: "publicKey2",
+ Name: "name_1",
+ }
+
+ id, err = keyRepository.Save(context.Background(), storage.Pgx, &key2)
+ require.Error(t, err)
+ require.Equal(t, uuid.Nil, id)
+ })
+}
+
+func TestKey_GetByPublicKey(t *testing.T) {
+ keyRepository := NewKey(*storage)
+ ctx := context.Background()
+ did := randomDID(t)
+ _, err := storage.Pgx.Exec(ctx, "INSERT INTO identities (identifier, keytype) VALUES ($1, $2)", did.String(), "BJJ")
+ assert.NoError(t, err)
+
+ key := domain.Key{
+ ID: uuid.New(),
+ IssuerDID: domain.KeyCoreDID(did),
+ PublicKey: "publicKey",
+ Name: "name" + uuid.New().String(),
+ }
+ id, err := keyRepository.Save(context.Background(), storage.Pgx, &key)
+ require.NoError(t, err)
+ require.NotEqual(t, uuid.Nil, id)
+ assert.Equal(t, key.ID, id)
+
+ t.Run("should get the key by public key", func(t *testing.T) {
+ keyFromDatabase, err := keyRepository.GetByPublicKey(ctx, did, key.PublicKey)
+ require.NoError(t, err)
+ assert.Equal(t, key.ID, keyFromDatabase.ID)
+ assert.Equal(t, key.IssuerDID, keyFromDatabase.IssuerDID)
+ assert.Equal(t, key.PublicKey, keyFromDatabase.PublicKey)
+ assert.Equal(t, key.Name, keyFromDatabase.Name)
+ })
+
+ t.Run("should get an error - ErrKeyNotFound", func(t *testing.T) {
+ keyFromDatabase, err := keyRepository.GetByPublicKey(ctx, did, "wrong public key")
+ assert.Error(t, err)
+ assert.Nil(t, keyFromDatabase)
+ assert.True(t, errors.Is(err, ErrKeyNotFound))
+ })
+}
diff --git a/internal/repositories/main_test.go b/internal/repositories/main_test.go
index d58adcca7..d208d681e 100644
--- a/internal/repositories/main_test.go
+++ b/internal/repositories/main_test.go
@@ -2,9 +2,14 @@ package repositories
import (
"context"
+ "crypto/rand"
"os"
"testing"
+ core "github.com/iden3/go-iden3-core/v2"
+ "github.com/iden3/go-iden3-core/v2/w3c"
+ "github.com/stretchr/testify/require"
+
"github.com/polygonid/sh-id-platform/internal/config"
"github.com/polygonid/sh-id-platform/internal/db"
"github.com/polygonid/sh-id-platform/internal/db/tests"
@@ -47,3 +52,16 @@ func lookupPostgresURL() string {
}
return con
}
+
+func randomDID(t *testing.T) w3c.DID {
+ t.Helper()
+ typ, err := core.BuildDIDType(core.DIDMethodIden3, core.Privado, core.Main)
+ var genesis [27]byte
+ require.NoError(t, err)
+ _, err = rand.Read(genesis[:])
+ require.NoError(t, err)
+ id := core.NewID(typ, genesis)
+ did, err := core.ParseDIDFromID(id)
+ require.NoError(t, err)
+ return *did
+}
diff --git a/internal/utils/public_key.go b/internal/utils/public_key.go
new file mode 100644
index 000000000..adf8ec793
--- /dev/null
+++ b/internal/utils/public_key.go
@@ -0,0 +1,58 @@
+package utils
+
+import (
+ "bytes"
+
+ "github.com/iden3/go-iden3-crypto/babyjub"
+ "github.com/iden3/go-schema-processor/v2/verifiable"
+
+ "github.com/polygonid/sh-id-platform/internal/core/domain"
+)
+
+// PublicKey - defines the interface for public keys
+type PublicKey interface {
+ Equal([]byte) bool
+ String() string
+}
+
+type bjjPublicKey struct {
+ publicKey babyjub.PublicKey
+}
+
+// newBJJPublicKey creates a new PublicKey from a Claim
+func newBJJPublicKey(claim domain.Claim) PublicKey {
+ entry := claim.CoreClaim.Get()
+ bjjClaim := entry.RawSlotsAsInts()
+ var authCoreClaimPublicKey babyjub.PublicKey
+ authCoreClaimPublicKey.X = bjjClaim[2]
+ authCoreClaimPublicKey.Y = bjjClaim[3]
+ return &bjjPublicKey{publicKey: authCoreClaimPublicKey}
+}
+
+func (b *bjjPublicKey) Equal(pubKey []byte) bool {
+ compPubKey := b.publicKey.Compress()
+ return bytes.Equal(pubKey, compPubKey[:])
+}
+
+func (b *bjjPublicKey) String() string {
+ return "0x" + b.publicKey.String()
+}
+
+type unSupportedPublicKeyType struct{}
+
+func (u *unSupportedPublicKeyType) Equal([]byte) bool {
+ return false
+}
+
+func (u *unSupportedPublicKeyType) String() string {
+ return ""
+}
+
+// GetPublicKeyFromClaim returns the public key of the claim
+// If the schema is not supported, it returns an unSupportedPublicKeyType
+func GetPublicKeyFromClaim(c *domain.Claim) PublicKey {
+ if c.SchemaURL == verifiable.JSONSchemaIden3AuthBJJCredential {
+ return newBJJPublicKey(*c)
+ }
+ return &unSupportedPublicKeyType{}
+}
diff --git a/k8s/helm/templates/issuer-node-ui-deployment.yaml b/k8s/helm/templates/issuer-node-ui-deployment.yaml
index 2a8b1d1c6..1ca56222c 100644
--- a/k8s/helm/templates/issuer-node-ui-deployment.yaml
+++ b/k8s/helm/templates/issuer-node-ui-deployment.yaml
@@ -25,7 +25,7 @@ spec:
image: {{ .Values.issuernode_ui_repository_image }}:{{ .Values.issuernode_ui_repository_tag }}
imagePullPolicy: {{ .Values.uiIssuerNode.deployment.imagePullPolicy | quote }}
ports:
- - containerPort: {{ .Values.uiIssuerNode.deployment.containerPort }}
+ - containerPort: {{ .Values.uiIssuerNode.deployment.containerPort }}
envFrom:
- - configMapRef:
- name: {{ .Values.uiIssuerNode.deployment.uiconfigMapRef }}
\ No newline at end of file
+ - configMapRef:
+ name: {{ .Values.uiIssuerNode.deployment.uiconfigMapRef }}
\ No newline at end of file
diff --git a/ui/package-lock.json b/ui/package-lock.json
index 6427b3cda..b76a8a5a0 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -8,6 +8,7 @@
"name": "issuer-node-ui",
"version": "1.0.0",
"dependencies": {
+ "@iden3/js-crypto": "^1.1.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"ajv-formats-draft2019": "^1.6.1",
@@ -1240,6 +1241,12 @@
"typescript": ">=5"
}
},
+ "node_modules/@iden3/js-crypto": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@iden3/js-crypto/-/js-crypto-1.1.0.tgz",
+ "integrity": "sha512-MbL7OpOxBoCybAPoorxrp+fwjDVESyDe6giIWxErjEIJy0Q2n1DU4VmKh4vDoCyhJx/RdVgT8Dkb59lKwISqsw==",
+ "license": "AGPL-3.0"
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
diff --git a/ui/package.json b/ui/package.json
index ad9f459d1..6bf0d357e 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -2,6 +2,7 @@
"name": "issuer-node-ui",
"version": "1.0.0",
"dependencies": {
+ "@iden3/js-crypto": "^1.1.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"ajv-formats-draft2019": "^1.6.1",
diff --git a/ui/src/adapters/api/credentials.ts b/ui/src/adapters/api/credentials.ts
index 45ca7bb5a..1082dc0b5 100644
--- a/ui/src/adapters/api/credentials.ts
+++ b/ui/src/adapters/api/credentials.ts
@@ -12,14 +12,17 @@ import {
serializeSorters,
} from "src/adapters/api";
import {
+ buildAppError,
datetimeParser,
getListParser,
getResourceParser,
getStrictParser,
} from "src/adapters/parsers";
import {
+ AuthCredential,
Credential,
CredentialDisplayMethod,
+ CredentialStatusType,
DisplayMethodType,
Env,
IssuedMessage,
@@ -42,12 +45,14 @@ type CredentialInput = Pick & {
} & Record;
credentialStatus: {
revocationNonce: number;
+ type: CredentialStatusType;
} & Record;
credentialSubject: Record;
displayMethod?: CredentialDisplayMethod | null;
expirationDate?: string | null;
issuanceDate: string;
issuer: string;
+ proof?: Array<{ type: ProofType }> | null;
refreshService?: RefreshService | null;
type: [string, string];
};
@@ -69,6 +74,7 @@ export const credentialParser = getStrictParser()(
credentialStatus: z
.object({
revocationNonce: z.number(),
+ type: z.nativeEnum(CredentialStatusType),
})
.and(z.record(z.unknown())),
credentialSubject: z.record(z.unknown()),
@@ -79,6 +85,14 @@ export const credentialParser = getStrictParser()(
expirationDate: datetimeParser.nullable().default(null),
issuanceDate: datetimeParser,
issuer: z.string(),
+ proof: z
+ .array(
+ z.object({
+ type: z.nativeEnum(ProofType),
+ })
+ )
+ .nullable()
+ .default(null),
refreshService: z
.object({ id: z.string(), type: z.literal("Iden3RefreshService2023") })
.nullable()
@@ -100,6 +114,7 @@ export const credentialParser = getStrictParser()(
expirationDate,
issuanceDate,
issuer,
+ proof,
refreshService,
type,
},
@@ -108,12 +123,14 @@ export const credentialParser = getStrictParser()(
const [, schemaType] = type;
return {
+ credentialStatus,
credentialSubject,
displayMethod,
expirationDate,
expired,
id,
issuanceDate,
+ proof,
proofTypes,
refreshService,
revNonce: credentialStatus.revocationNonce,
@@ -133,6 +150,27 @@ export const credentialStatusParser = getStrictParser()(
z.union([z.literal("all"), z.literal("revoked"), z.literal("expired")])
);
+export type AuthCredentialSubjectInput = {
+ x: string;
+ y: string;
+};
+export type AuthCredentialSubject = {
+ x: bigint;
+ y: bigint;
+};
+
+export const authCredentialSubjectParser = getStrictParser<
+ AuthCredentialSubjectInput,
+ AuthCredentialSubject
+>()(
+ z
+ .object({
+ x: z.string().regex(/^\d+$/, "x must be a numeric string"),
+ y: z.string().regex(/^\d+$/, "y must be a numeric string"),
+ })
+ .transform(({ x, y }) => ({ x: BigInt(x), y: BigInt(y) }))
+);
+
export async function getCredential({
credentialID,
env,
@@ -202,6 +240,65 @@ export async function getCredentials({
}
}
+export async function getAuthCredentialsByIDs({
+ env,
+ identifier,
+ IDs,
+ signal,
+}: {
+ IDs: Array;
+ env: Env;
+ identifier: string;
+ signal?: AbortSignal;
+}): Promise>> {
+ try {
+ const promises = IDs.map((id) => getCredential({ credentialID: id, env, identifier, signal }));
+ const credentials = await Promise.all(promises);
+
+ const { failed, successful } = credentials.reduce>(
+ (acc, credential) => {
+ try {
+ if (credential.success) {
+ const parsedCredentialSubject = authCredentialSubjectParser.parse({
+ x: credential.data.credentialSubject.x,
+ y: credential.data.credentialSubject.y,
+ });
+
+ const published =
+ credential.data.proof?.some(
+ ({ type }) => type === ProofType.Iden3SparseMerkleTreeProof
+ ) || false;
+
+ return {
+ ...acc,
+ successful: [
+ ...acc.successful,
+ {
+ ...credential.data,
+ credentialSubject: { ...parsedCredentialSubject },
+ published,
+ },
+ ],
+ };
+ } else {
+ return { ...acc, failed: [...acc.failed, credential.error] };
+ }
+ } catch (error) {
+ return { ...acc, failed: [...acc.failed, buildAppError(error)] };
+ }
+ },
+ { failed: [], successful: [] }
+ );
+
+ return buildSuccessResponse({
+ failed,
+ successful,
+ });
+ } catch (error) {
+ return buildErrorResponse(error);
+ }
+}
+
export type CreateCredential = {
credentialSchema: string;
credentialSubject: Json;
@@ -579,3 +676,33 @@ export async function getIssuedMessages({
return buildErrorResponse(error);
}
}
+
+export type CreateAuthCredential = {
+ credentialStatusType: CredentialStatusType;
+ keyID: string;
+};
+
+export async function createAuthCredential({
+ env,
+ identifier,
+ payload,
+}: {
+ env: Env;
+ identifier: string;
+ payload: CreateAuthCredential;
+}): Promise> {
+ try {
+ const response = await axios({
+ baseURL: env.api.url,
+ data: payload,
+ headers: {
+ Authorization: buildAuthorizationHeader(env),
+ },
+ method: "POST",
+ url: `${API_VERSION}/identities/${identifier}/create-auth-credential`,
+ });
+ return buildSuccessResponse(IDParser.parse(response.data));
+ } catch (error) {
+ return buildErrorResponse(error);
+ }
+}
diff --git a/ui/src/adapters/api/identities.ts b/ui/src/adapters/api/identities.ts
index 8e298c0d8..6303290d8 100644
--- a/ui/src/adapters/api/identities.ts
+++ b/ui/src/adapters/api/identities.ts
@@ -49,7 +49,7 @@ export async function getIdentities({
url: `${API_VERSION}/identities`,
});
- return buildSuccessResponse(getListParser(identityParser).parse(response.data || []));
+ return buildSuccessResponse(getListParser(identityParser).parse(response.data));
} catch (error) {
return buildErrorResponse(error);
}
@@ -101,6 +101,7 @@ export async function createIdentity({
export const identityDetailsParser = getStrictParser()(
z.object({
+ authCredentialsIDs: z.array(z.string()),
credentialStatusType: z.nativeEnum(CredentialStatusType),
displayName: z.string().nullable(),
identifier: z.string(),
diff --git a/ui/src/adapters/api/keys.ts b/ui/src/adapters/api/keys.ts
new file mode 100644
index 000000000..194bd9b3d
--- /dev/null
+++ b/ui/src/adapters/api/keys.ts
@@ -0,0 +1,168 @@
+import axios from "axios";
+import { z } from "zod";
+
+import { Response, buildErrorResponse, buildSuccessResponse } from "src/adapters";
+import { ID, IDParser, Message, buildAuthorizationHeader, messageParser } from "src/adapters/api";
+import { getResourceParser, getStrictParser } from "src/adapters/parsers";
+import { Env, Key, KeyType } from "src/domain";
+import { API_VERSION } from "src/utils/constants";
+import { Resource } from "src/utils/types";
+
+const keyParser = getStrictParser()(
+ z.object({
+ id: z.string(),
+ isAuthCredential: z.boolean(),
+ keyType: z.nativeEnum(KeyType),
+ name: z.string(),
+ publicKey: z.string(),
+ })
+);
+
+export async function getKeys({
+ env,
+ identifier,
+ params: { maxResults, page, type },
+ signal,
+}: {
+ env: Env;
+ identifier: string;
+ params: {
+ maxResults?: number;
+ page?: number;
+ type?: KeyType;
+ };
+ signal?: AbortSignal;
+}): Promise>> {
+ try {
+ const response = await axios({
+ baseURL: env.api.url,
+ headers: {
+ Authorization: buildAuthorizationHeader(env),
+ },
+ method: "GET",
+ params: new URLSearchParams({
+ ...(maxResults !== undefined ? { max_results: maxResults.toString() } : {}),
+ ...(page !== undefined ? { page: page.toString() } : {}),
+ ...(type !== undefined ? { type } : {}),
+ }),
+ signal,
+ url: `${API_VERSION}/identities/${identifier}/keys`,
+ });
+ return buildSuccessResponse(getResourceParser(keyParser).parse(response.data));
+ } catch (error) {
+ return buildErrorResponse(error);
+ }
+}
+
+export async function getKey({
+ env,
+ identifier,
+ keyID,
+ signal,
+}: {
+ env: Env;
+ identifier: string;
+ keyID: string;
+ signal?: AbortSignal;
+}): Promise> {
+ try {
+ const response = await axios({
+ baseURL: env.api.url,
+ headers: {
+ Authorization: buildAuthorizationHeader(env),
+ },
+ method: "GET",
+ signal,
+ url: `${API_VERSION}/identities/${identifier}/keys/${keyID}`,
+ });
+ return buildSuccessResponse(keyParser.parse(response.data));
+ } catch (error) {
+ return buildErrorResponse(error);
+ }
+}
+
+export type CreateKey = {
+ keyType: KeyType;
+ name: string;
+};
+
+export async function createKey({
+ env,
+ identifier,
+ payload,
+}: {
+ env: Env;
+ identifier: string;
+ payload: CreateKey;
+}): Promise> {
+ try {
+ const response = await axios({
+ baseURL: env.api.url,
+ data: payload,
+ headers: {
+ Authorization: buildAuthorizationHeader(env),
+ },
+ method: "POST",
+ url: `${API_VERSION}/identities/${identifier}/keys`,
+ });
+ return buildSuccessResponse(IDParser.parse(response.data));
+ } catch (error) {
+ return buildErrorResponse(error);
+ }
+}
+
+export type UpdateKey = {
+ name: string;
+};
+
+export async function updateKeyName({
+ env,
+ identifier,
+ keyID,
+ payload,
+}: {
+ env: Env;
+ identifier: string;
+ keyID: string;
+ payload: UpdateKey;
+}) {
+ try {
+ await axios({
+ baseURL: env.api.url,
+ data: payload,
+ headers: {
+ Authorization: buildAuthorizationHeader(env),
+ },
+ method: "PATCH",
+ url: `${API_VERSION}/identities/${identifier}/keys/${keyID}`,
+ });
+
+ return buildSuccessResponse(undefined);
+ } catch (error) {
+ return buildErrorResponse(error);
+ }
+}
+
+export async function deleteKey({
+ env,
+ identifier,
+ keyID,
+}: {
+ env: Env;
+ identifier: string;
+ keyID: string;
+}): Promise> {
+ try {
+ const response = await axios({
+ baseURL: env.api.url,
+ headers: {
+ Authorization: buildAuthorizationHeader(env),
+ },
+ method: "DELETE",
+ url: `${API_VERSION}/identities/${identifier}/keys/${keyID}`,
+ });
+ return buildSuccessResponse(messageParser.parse(response.data));
+ } catch (error) {
+ return buildErrorResponse(error);
+ }
+}
diff --git a/ui/src/adapters/index.ts b/ui/src/adapters/index.ts
index e566ba8ed..1c683edad 100644
--- a/ui/src/adapters/index.ts
+++ b/ui/src/adapters/index.ts
@@ -1,5 +1,5 @@
+import { buildAppError } from "src/adapters/parsers";
import { AppError } from "src/domain";
-import { buildAppError } from "src/utils/error";
type SuccessResponse = {
data: D;
diff --git a/ui/src/adapters/jsonSchemas.ts b/ui/src/adapters/jsonSchemas.ts
index f3ed5c401..1c75f89a8 100644
--- a/ui/src/adapters/jsonSchemas.ts
+++ b/ui/src/adapters/jsonSchemas.ts
@@ -1,8 +1,8 @@
import { Response } from "src/adapters";
import { getJsonFromUrl } from "src/adapters/json";
+import { buildAppError } from "src/adapters/parsers";
import { getJsonLdTypeParser, jsonSchemaParser } from "src/adapters/parsers/jsonSchemas";
import { Env, Json, JsonLdType, JsonSchema } from "src/domain";
-import { buildAppError } from "src/utils/error";
export async function getJsonSchemaFromUrl({
env,
diff --git a/ui/src/adapters/parsers/index.ts b/ui/src/adapters/parsers/index.ts
index ed71013e2..643d3f42a 100644
--- a/ui/src/adapters/parsers/index.ts
+++ b/ui/src/adapters/parsers/index.ts
@@ -1,5 +1,9 @@
+import { message } from "antd";
+import { MessageType } from "antd/es/message/interface";
+import { isAxiosError, isCancel } from "axios";
import { z } from "zod";
+import { AppError } from "src/domain";
import { List, ResourceMeta } from "src/utils/types";
export function getListParser(
@@ -11,23 +15,30 @@ export function getListParser(
(acc: List